mirror of
https://github.com/apache/superset.git
synced 2026-06-11 02:29:19 +00:00
Compare commits
20 Commits
fix/smtp-s
...
fix/chart-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d544bff071 | ||
|
|
f79a88c685 | ||
|
|
b1d965932d | ||
|
|
7d046340dc | ||
|
|
aa872cd0a1 | ||
|
|
b2c5a1ecb3 | ||
|
|
6cd9bdee0b | ||
|
|
a8a1d9c17d | ||
|
|
97058d2cf0 | ||
|
|
ef57409209 | ||
|
|
5f06e66cf1 | ||
|
|
11af932099 | ||
|
|
c9c05d8d0a | ||
|
|
0f59705806 | ||
|
|
320965612d | ||
|
|
c3df60c12b | ||
|
|
4f69949c10 | ||
|
|
3380496e9f | ||
|
|
248ccadecd | ||
|
|
cc5a3ddd05 |
@@ -64,7 +64,7 @@ dependencies = [
|
||||
"holidays>=0.45, <1",
|
||||
"humanize",
|
||||
"isodate",
|
||||
"jsonpath-ng>=1.6.1, <2",
|
||||
"jsonpath-ng>=1.8.0, <2",
|
||||
"Mako>=1.2.2",
|
||||
"markdown>=3.10.2",
|
||||
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
|
||||
@@ -94,7 +94,7 @@ dependencies = [
|
||||
"PyJWT>=2.4.0, <3.0",
|
||||
"redis>=5.0.0, <6.0",
|
||||
"rison>=2.0.0, <3.0",
|
||||
"selenium>=4.14.0, <5.0",
|
||||
"selenium>=4.44.0, <5.0",
|
||||
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
|
||||
"sshtunnel>=0.4.0, <0.5",
|
||||
"simplejson>=3.15.0",
|
||||
@@ -107,7 +107,7 @@ dependencies = [
|
||||
"typing-extensions>=4, <5",
|
||||
"waitress; sys_platform == 'win32'",
|
||||
"watchdog>=6.0.0",
|
||||
"wtforms>=2.3.3, <4",
|
||||
"wtforms>=3.2.2, <4",
|
||||
"wtforms-json",
|
||||
"xlsxwriter>=3.2.9, <3.3",
|
||||
]
|
||||
@@ -121,7 +121,7 @@ bigquery = [
|
||||
"sqlalchemy-bigquery>=1.15.0",
|
||||
"google-cloud-bigquery>=3.10.0",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
|
||||
clickhouse = ["clickhouse-connect>=1.1.1, <2.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
|
||||
d1 = [
|
||||
@@ -161,7 +161,7 @@ hive = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7.0",
|
||||
"tableschema",
|
||||
"thrift>=0.14.1, <1.0.0",
|
||||
"thrift>=0.23.0, <1.0.0",
|
||||
"thrift_sasl>=0.4.3, < 1.0.0",
|
||||
]
|
||||
impala = ["impyla>0.16.2, <0.23"]
|
||||
@@ -195,7 +195,7 @@ spark = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7",
|
||||
"tableschema",
|
||||
"thrift>=0.14.1, <1",
|
||||
"thrift>=0.23.0, <1",
|
||||
]
|
||||
tdengine = [
|
||||
"taospy>=2.7.21",
|
||||
|
||||
@@ -50,7 +50,7 @@ cattrs==25.1.1
|
||||
# via requests-cache
|
||||
celery==5.5.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
certifi==2025.6.15
|
||||
certifi==2026.5.20
|
||||
# via
|
||||
# requests
|
||||
# selenium
|
||||
@@ -194,7 +194,7 @@ jinja2==3.1.6
|
||||
# via
|
||||
# flask
|
||||
# flask-babel
|
||||
jsonpath-ng==1.7.0
|
||||
jsonpath-ng==1.8.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
jsonschema==4.23.0
|
||||
# via
|
||||
@@ -286,8 +286,6 @@ pillow==12.2.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
ply==3.11
|
||||
# via jsonpath-ng
|
||||
polyline==2.0.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
prison==0.2.1
|
||||
@@ -380,7 +378,7 @@ rpds-py==0.25.0
|
||||
# referencing
|
||||
rsa==4.9.1
|
||||
# via google-auth
|
||||
selenium==4.32.0
|
||||
selenium==4.44.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
setuptools==80.9.0
|
||||
# via -r requirements/base.in
|
||||
@@ -423,7 +421,7 @@ sshtunnel==0.4.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
tabulate==0.10.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
trio==0.30.0
|
||||
trio==0.33.0
|
||||
# via
|
||||
# selenium
|
||||
# trio-websocket
|
||||
@@ -480,7 +478,7 @@ wrapt==1.17.2
|
||||
# via deprecated
|
||||
wsproto==1.2.0
|
||||
# via trio-websocket
|
||||
wtforms==3.2.1
|
||||
wtforms==3.2.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
|
||||
@@ -112,7 +112,7 @@ celery==5.5.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
certifi==2025.6.15
|
||||
certifi==2026.5.20
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# httpcore
|
||||
@@ -471,7 +471,7 @@ jmespath==1.1.0
|
||||
# via
|
||||
# boto3
|
||||
# botocore
|
||||
jsonpath-ng==1.7.0
|
||||
jsonpath-ng==1.8.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -674,10 +674,6 @@ platformdirs==4.3.8
|
||||
# virtualenv
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
ply==3.11
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# jsonpath-ng
|
||||
polib==1.2.0
|
||||
# via apache-superset
|
||||
polyline==2.0.2
|
||||
@@ -925,7 +921,7 @@ s3transfer==0.16.0
|
||||
# via boto3
|
||||
secretstorage==3.5.0
|
||||
# via keyring
|
||||
selenium==4.32.0
|
||||
selenium==4.44.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -1023,7 +1019,7 @@ tqdm==4.67.1
|
||||
# prophet
|
||||
trino==0.330.0
|
||||
# via apache-superset
|
||||
trio==0.30.0
|
||||
trio==0.33.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# selenium
|
||||
@@ -1125,7 +1121,7 @@ wsproto==1.2.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# trio-websocket
|
||||
wtforms==3.2.1
|
||||
wtforms==3.2.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -48,6 +48,7 @@ module.exports = {
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-export-namespace-from',
|
||||
['@babel/plugin-transform-class-properties', { loose: true }],
|
||||
'@babel/plugin-transform-class-static-block',
|
||||
['@babel/plugin-transform-optional-chaining', { loose: true }],
|
||||
['@babel/plugin-transform-private-methods', { loose: true }],
|
||||
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],
|
||||
|
||||
234
superset-frontend/package-lock.json
generated
234
superset-frontend/package-lock.json
generated
@@ -95,7 +95,7 @@
|
||||
"echarts": "^5.6.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.3.5",
|
||||
"fuse.js": "^7.3.0",
|
||||
"fuse.js": "^7.4.1",
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
"geostyler-data": "^1.1.0",
|
||||
@@ -178,13 +178,13 @@
|
||||
"@babel/types": "^7.29.7",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@formatjs/intl-durationformat": "^0.10.3",
|
||||
"@formatjs/intl-durationformat": "^0.10.13",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.4.1",
|
||||
"@storybook/addon-links": "10.4.1",
|
||||
"@storybook/react-webpack5": "10.4.1",
|
||||
"@storybook/addon-docs": "10.4.2",
|
||||
"@storybook/addon-links": "10.4.2",
|
||||
"@storybook/react-webpack5": "10.4.2",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.40",
|
||||
@@ -242,7 +242,7 @@
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.4",
|
||||
"eslint-plugin-storybook": "10.4.1",
|
||||
"eslint-plugin-storybook": "10.4.2",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -272,7 +272,7 @@
|
||||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.4.1",
|
||||
"storybook": "10.4.2",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
@@ -3939,50 +3939,38 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/bigdecimal": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.0.tgz",
|
||||
"integrity": "sha512-GeaxHZbUoYvHL9tC5eltHLs+1zU70aPw0s7LwqgktIzF5oMhNY4o4deEtusJMsq7WFJF3Ye2zQEzdG8beVk73w==",
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.5.tgz",
|
||||
"integrity": "sha512-2XTKNrZRaCUyXK2976wfutqxMBuPO/S/zbJnQdysLI2Zy5mWPVNVEkE6tsTcSVWSE7DgO88t8DtBy+uf3I8bxg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.2.0.tgz",
|
||||
"integrity": "sha512-dHnqHgBo6GXYGRsepaE1wmsC2etaivOWd5VaJstZd+HI2zR3DCUjbDVZRtoPGkkXZmyHvBwrdEUuqfvzhF/DtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/bigdecimal": "0.2.0",
|
||||
"@formatjs/fast-memoize": "3.1.1",
|
||||
"@formatjs/intl-localematcher": "0.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/fast-memoize": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.1.tgz",
|
||||
"integrity": "sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.5.tgz",
|
||||
"integrity": "sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@formatjs/intl-durationformat": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.3.tgz",
|
||||
"integrity": "sha512-xRS3GaOlsQLwz0n56SvaddwEnl2NLPKBvYg2M32ak/27dodmVxFJz3j7Nqj7EwKyHTu3f/e+BeoKPrIDUSXTuQ==",
|
||||
"version": "0.10.13",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.13.tgz",
|
||||
"integrity": "sha512-A1dBcOh1YrcRf/AbmZHFVXgIYkpAaFgyGaYavO/KutbqEXY3HI63o2E1ctmxmllfg3qn3TZGtZux42EFwHNTbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "3.2.0",
|
||||
"@formatjs/intl-localematcher": "0.8.2"
|
||||
"@formatjs/bigdecimal": "0.2.5",
|
||||
"@formatjs/intl-localematcher": "0.8.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.2.tgz",
|
||||
"integrity": "sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==",
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.9.tgz",
|
||||
"integrity": "sha512-GmB0F/gYh4Hdl4rLWjgDsgT+x4pB54fkJeRh8kAZ4XFzKeCK8dGs+SBJWXO42QZtOUni+IDWKNuCw6wiL4lTvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "3.1.1"
|
||||
"@formatjs/fast-memoize": "3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@gar/promise-retry": {
|
||||
@@ -9696,16 +9684,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.1.tgz",
|
||||
"integrity": "sha512-IYqUdjoZe4VO2LFZlKL/gwy7DsQSWCq6hX+zc1MBmZo04yycDASk1tte57n9pdlW3ajw9yYMF/+lVBi+xQjyvw==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.2.tgz",
|
||||
"integrity": "sha512-CtW1O4xSKZPNtpWgpfp4yB/x4pj/of+3MvlEDfErSlr3Hp3QmEa2pCLaecR08H5LJqJFlt1PtG0UrIynTvgW9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.4.1",
|
||||
"@storybook/csf-plugin": "10.4.2",
|
||||
"@storybook/icons": "^2.0.2",
|
||||
"@storybook/react-dom-shim": "10.4.1",
|
||||
"@storybook/react-dom-shim": "10.4.2",
|
||||
"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"
|
||||
@@ -9716,7 +9704,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.1"
|
||||
"storybook": "^10.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -9724,45 +9712,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.1.tgz",
|
||||
"integrity": "sha512-WdPepGBxDGOUDjYd8KxMtcf+us/2PAcnBczl77XtrnxxHNs0jWesxKkiJ9yiuGrge4BPhDeAj6rxjbBoaHxLBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unplugin": "^2.3.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "^10.4.1",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"rollup": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
},
|
||||
"webpack": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.1.tgz",
|
||||
"integrity": "sha512-h/5D23GwMuHA55sB7XDyhByF9psF7UFmaQOn72pjNAarew5eOpue5A+jXk3AKEYokHbvgQaoz+FrvWo9GEfSKQ==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.2.tgz",
|
||||
"integrity": "sha512-cU8h4/m+oAr8UUwF4teZG2N1ilV+vU+98Ii/Ma+IIx9M/V7i5544UxfAz84dV5Rx2Oho6x8XH3gIvmevSyPi/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9775,7 +9728,7 @@
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.1"
|
||||
"storybook": "^10.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -9787,13 +9740,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-webpack5": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.1.tgz",
|
||||
"integrity": "sha512-3Ah4jUjg8nEms/5JV6odtQj9+pQ1DT/04s/V6dZKThGdl85YTrYUZV5OTgbNxYbmQn/TwpWWjQlcW8ulpo2WBw==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.2.tgz",
|
||||
"integrity": "sha512-nhmV0+nThCgy1y5742SS7c4vJrd5/1KfCXCNfsJ1v4Rkq7NIQnUhEIBwkSaY63lqH7FRHlFxIjwGS63veiCJuw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.4.1",
|
||||
"@storybook/core-webpack": "10.4.2",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||
"cjs-module-lexer": "^1.2.3",
|
||||
"css-loader": "^7.1.2",
|
||||
@@ -9814,7 +9767,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.1"
|
||||
"storybook": "^10.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -9823,9 +9776,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core-webpack": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.1.tgz",
|
||||
"integrity": "sha512-Wert/4ou5WRl8WYWWS8bBW7Lxa/ASMEuQ3EVuG3SITAtPNvKDKqTFBjZLx9eJSefkX6fJ3yG85FFUOPsv6GemQ==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.2.tgz",
|
||||
"integrity": "sha512-qnYKMruU8lvI4yaq2PA9Gmxjrc7EZ3DRBI/cVKwEgOIREoxzr1F1IE7t7+325k9Phylue7E5rD3A7yjxeEKUyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9836,7 +9789,42 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.1"
|
||||
"storybook": "^10.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.2.tgz",
|
||||
"integrity": "sha512-GqX/2DeF3/jKs5D7gpDiuT9gd0c/f2TKcnQ5av4/s3YqeN+0nhm7btkCrDfgF16uzE1Zj3OrkxvB3AOkfxWgDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unplugin": "^2.3.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "^10.4.2",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"rollup": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
},
|
||||
"webpack": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/global": {
|
||||
@@ -9858,13 +9846,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preset-react-webpack": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.1.tgz",
|
||||
"integrity": "sha512-uAR/C/oDZYhReaYpD4Rd5S4VWcXP2XO8+BwXwanKt4UHbYfOw7AQgBTeZ/6Wns/0xIXhOoA1rxO5TA2wDLUjLA==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.2.tgz",
|
||||
"integrity": "sha512-21ld380f0/jTTitkfhTKgP3FBnVAgMu1P1ymrRyiFYJVSJBA5YejndFFBo0ugq9iGGsHXrVdOphC/OJKbTSWRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.4.1",
|
||||
"@storybook/core-webpack": "10.4.2",
|
||||
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"magic-string": "^0.30.5",
|
||||
@@ -9881,7 +9869,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.1"
|
||||
"storybook": "^10.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -9890,14 +9878,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.1.tgz",
|
||||
"integrity": "sha512-WuYz4NaUk4gmFAMliSpCbV8w6jP5OY9juBfw1huwzu2S/k5FhnVXwmrUaL0fmf3Bq/7NgkzmBBbZr6I6LuHayQ==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.2.tgz",
|
||||
"integrity": "sha512-NfEH3CrdCAgUV4Z7SPN3Iw6nofcueqtRj8iHuo77GNjz0qSfuVi9iS7a8o7x7QFSeIBZwS0Jv3CgmhN8qvoLjg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "10.4.1",
|
||||
"@storybook/react-dom-shim": "10.4.2",
|
||||
"react-docgen": "^8.0.2",
|
||||
"react-docgen-typescript": "^2.2.2"
|
||||
},
|
||||
@@ -9910,7 +9898,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.1",
|
||||
"storybook": "^10.4.2",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -9990,9 +9978,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.1.tgz",
|
||||
"integrity": "sha512-6QFqfDNH4DMrt7yHKRfpqRopsVUc/Az+sXIdJ39IetYnHUxL3nW4NVaPc6uy/8Qi8urzUyEXL/nn7cpSIP2aPQ==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.2.tgz",
|
||||
"integrity": "sha512-Eng3Yt2NCjPX94QcfyLeUFhrMj0hec2yU9J/qafBVbfj9XrFI8o+0ZwYJ7uXb9ECbvPN4y06dgt/2W/LiR417w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -10004,7 +9992,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.1"
|
||||
"storybook": "^10.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -10016,15 +10004,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.4.1.tgz",
|
||||
"integrity": "sha512-2jF231DrEk70I8+wVakCnKtpweGFNfxdaov883Rve0TFvhxZs42Y9PpKzSf4rusvSrWc9jdWuJ2k7ERbS50MLg==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.4.2.tgz",
|
||||
"integrity": "sha512-x7xwGLxU0w6/qi29/cHhua8qiCvfE05ku4pPLTXF8TsP/zfGsY8tbdlKO2+YKp+iBG8vafVc//ZXOAty1oypDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/builder-webpack5": "10.4.1",
|
||||
"@storybook/preset-react-webpack": "10.4.1",
|
||||
"@storybook/react": "10.4.1"
|
||||
"@storybook/builder-webpack5": "10.4.2",
|
||||
"@storybook/preset-react-webpack": "10.4.2",
|
||||
"@storybook/react": "10.4.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -10033,7 +10021,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.1",
|
||||
"storybook": "^10.4.2",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -19421,9 +19409,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-storybook": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.4.1.tgz",
|
||||
"integrity": "sha512-sLEvd/7lg/LtXwMjj3iFxZtoeAC/8l1Qhuw3Noa8iF8i0UIgAejUs7k6DNSqHkwrPR8caWT4+3fxdMXs1iGLTg==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.4.2.tgz",
|
||||
"integrity": "sha512-l3/vzLRmb8VSi3X1Bo6/Pa+64naw1jFsZE5jPPA4izvVdNhH1rF4rGuOC3kDTU926qKVBQtKua8D24XWQtvcGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -19431,7 +19419,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8",
|
||||
"storybook": "^10.4.1"
|
||||
"storybook": "^10.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-testing-library": {
|
||||
@@ -20933,9 +20921,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fuse.js": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.4.0.tgz",
|
||||
"integrity": "sha512-3UqmoSFwzX1sNB1YSk+Co0EdH29XCW2p9g48OAiy93cjKqzuABsqw2VIgSN3CmsT/wo6pIJ3F0Jxeiiby8rhIQ==",
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.4.1.tgz",
|
||||
"integrity": "sha512-AY7lKAXK71hi3WgUvDy6oZL67UEHOOtvCAwVdOXHyJd6ZzftBy7QqxuXt4HxmmAhYjmp/YCuOELZtIvAdlZ+fw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -39048,9 +39036,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.1.tgz",
|
||||
"integrity": "sha512-V1Zd2e+gBFufqAQVZ1JR8KLqALsEZ3JYSBnWwQbKa6zCfWWanR6AFMyuOkLt2gZOgGp3h2Riuz88pGNVTQSG0A==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.2.tgz",
|
||||
"integrity": "sha512-5Ax5vbHxFgMBGGhQDm75Rrumm/HZC4ICFhMcJaM0UlqnC/4FKj/IaZtImZFupknyiiyUEcWHPQFA2kX3/VSv1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -45375,7 +45363,7 @@
|
||||
"@deck.gl/extensions": "~9.2.9",
|
||||
"@deck.gl/geo-layers": "~9.2.5",
|
||||
"@deck.gl/layers": "~9.2.5",
|
||||
"@deck.gl/mapbox": "~9.3.2",
|
||||
"@deck.gl/mapbox": "^9.3.3",
|
||||
"@deck.gl/mesh-layers": "~9.2.5",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
@@ -45423,9 +45411,9 @@
|
||||
}
|
||||
},
|
||||
"plugins/preset-chart-deckgl/node_modules/@deck.gl/mapbox": {
|
||||
"version": "9.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.3.2.tgz",
|
||||
"integrity": "sha512-+T9pJwsOXwjUxyGN6oiBMfIs28VtDIG1V1Rqz4qqn4TjjNEFFw+xO0olJIg8FO5IAqw2OtePdsrMj0tX8tHdGQ==",
|
||||
"version": "9.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.3.3.tgz",
|
||||
"integrity": "sha512-aUPqrwF6wkx+EtvKA3SaiK+UROMnZSmgEJWZ1qSKFSiH//kPuo5imbtXyan8sGhOet7NjnfEwJqFA3EBk7zDLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@math.gl/web-mercator": "^4.1.0"
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
"echarts": "^5.6.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.3.5",
|
||||
"fuse.js": "^7.3.0",
|
||||
"fuse.js": "^7.4.1",
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
"geostyler-data": "^1.1.0",
|
||||
@@ -261,13 +261,13 @@
|
||||
"@babel/types": "^7.29.7",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@formatjs/intl-durationformat": "^0.10.3",
|
||||
"@formatjs/intl-durationformat": "^0.10.13",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.4.1",
|
||||
"@storybook/addon-links": "10.4.1",
|
||||
"@storybook/react-webpack5": "10.4.1",
|
||||
"@storybook/addon-docs": "10.4.2",
|
||||
"@storybook/addon-links": "10.4.2",
|
||||
"@storybook/react-webpack5": "10.4.2",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.40",
|
||||
@@ -325,7 +325,7 @@
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.4",
|
||||
"eslint-plugin-storybook": "10.4.1",
|
||||
"eslint-plugin-storybook": "10.4.2",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -355,7 +355,7 @@
|
||||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.4.1",
|
||||
"storybook": "10.4.2",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { Disposable } from '../common';
|
||||
import { Disposable, Event } from '../common';
|
||||
|
||||
/**
|
||||
* Represents a menu item that links a view to a command.
|
||||
@@ -102,3 +102,37 @@ export declare function registerMenuItem(
|
||||
* ```
|
||||
*/
|
||||
export declare function getMenu(location: string): Menu | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when a menu item is registered.
|
||||
*/
|
||||
export interface MenuItemRegisteredEvent {
|
||||
/** The menu item that was registered. */
|
||||
item: MenuItem;
|
||||
/** The location where the item was registered. */
|
||||
location: string;
|
||||
/** The group the item was placed in. */
|
||||
group: 'primary' | 'secondary' | 'context';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when a menu item is unregistered.
|
||||
*/
|
||||
export interface MenuItemUnregisteredEvent {
|
||||
/** The menu item that was unregistered. */
|
||||
item: MenuItem;
|
||||
/** The location where the item was registered. */
|
||||
location: string;
|
||||
/** The group the item was placed in. */
|
||||
group: 'primary' | 'secondary' | 'context';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when a menu item is registered.
|
||||
*/
|
||||
export declare const onDidRegisterMenuItem: Event<MenuItemRegisteredEvent>;
|
||||
|
||||
/**
|
||||
* Event fired when a menu item is unregistered.
|
||||
*/
|
||||
export declare const onDidUnregisterMenuItem: Event<MenuItemUnregisteredEvent>;
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
*/
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { Disposable } from '../common';
|
||||
import { Disposable, Event } from '../common';
|
||||
|
||||
/**
|
||||
* Represents a contributed view in the application.
|
||||
@@ -88,3 +88,33 @@ export declare function registerView(
|
||||
* ```
|
||||
*/
|
||||
export declare function getViews(location: string): View[] | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when a view is registered.
|
||||
*/
|
||||
export interface ViewRegisteredEvent {
|
||||
/** The descriptor of the view that was registered. */
|
||||
view: View;
|
||||
/** The location where the view was registered. */
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when a view is unregistered.
|
||||
*/
|
||||
export interface ViewUnregisteredEvent {
|
||||
/** The descriptor of the view that was unregistered. */
|
||||
view: View;
|
||||
/** The location where the view was registered. */
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when a view is registered.
|
||||
*/
|
||||
export declare const onDidRegisterView: Event<ViewRegisteredEvent>;
|
||||
|
||||
/**
|
||||
* Event fired when a view is unregistered.
|
||||
*/
|
||||
export declare const onDidUnregisterView: Event<ViewUnregisteredEvent>;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"dompurify": "^3.4.7",
|
||||
"dompurify": "^3.4.8",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"jed": "^1.1.1",
|
||||
|
||||
@@ -35,7 +35,7 @@ test('format milliseconds in human readable format with default options', () =>
|
||||
});
|
||||
test('format seconds in human readable format with default options', () => {
|
||||
const formatter = createDurationFormatter({ multiplier: 1000 });
|
||||
expect(formatter(-0.5)).toBe('-0s');
|
||||
expect(formatter(-0.5)).toBe('0s');
|
||||
expect(formatter(0.5)).toBe('0s');
|
||||
expect(formatter(1)).toBe('1s');
|
||||
expect(formatter(30)).toBe('30s');
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.18.1",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"dompurify": "^3.4.7",
|
||||
"dompurify": "^3.4.8",
|
||||
"prop-types": "^15.8.1",
|
||||
"urijs": "^1.19.11"
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@deck.gl/extensions": "~9.2.9",
|
||||
"@deck.gl/geo-layers": "~9.2.5",
|
||||
"@deck.gl/layers": "~9.2.5",
|
||||
"@deck.gl/mapbox": "~9.3.2",
|
||||
"@deck.gl/mapbox": "~9.3.3",
|
||||
"@deck.gl/mesh-layers": "~9.2.5",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import {
|
||||
DataMaskStateWithId,
|
||||
ExtraFormData,
|
||||
Filter,
|
||||
NativeFiltersState,
|
||||
NativeFilterType,
|
||||
} from '@superset-ui/core';
|
||||
@@ -458,6 +459,25 @@ export const mockQueryDataForCountries = [
|
||||
{ country_name: 'Zimbabwe', 'SUM(SP_POP_TOTL)': 509866860 },
|
||||
];
|
||||
|
||||
export const createSelectNativeFilter = (
|
||||
id: string,
|
||||
name: string,
|
||||
column: string = name,
|
||||
): Filter => ({
|
||||
id,
|
||||
name,
|
||||
type: NativeFilterType.NativeFilter,
|
||||
filterType: 'filter_select',
|
||||
targets: [{ datasetId: 2, column: { name: column } }],
|
||||
defaultDataMask: { filterState: { value: null }, extraFormData: {} },
|
||||
controlValues: {},
|
||||
cascadeParentIds: [],
|
||||
scope: { rootPath: ['ROOT_ID'], excluded: [] },
|
||||
description: '',
|
||||
chartsInScope: [],
|
||||
tabsInScope: [],
|
||||
});
|
||||
|
||||
export const buildNativeFilter = (
|
||||
id: string,
|
||||
name: string,
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { SqlLabRootState } from 'src/SqlLab/types';
|
||||
import { css, styled } from '@apache-superset/core/theme';
|
||||
import { useComponentDidUpdate } from '@superset-ui/core';
|
||||
import { Grid } from '@superset-ui/core/components';
|
||||
import { views } from 'src/core';
|
||||
import { useViews } from 'src/core';
|
||||
import { Splitter } from 'src/components/Splitter';
|
||||
import useEffectEvent from 'src/hooks/useEffectEvent';
|
||||
import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth';
|
||||
@@ -96,7 +96,7 @@ const AppLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
setRightWidth(possibleRightWidth);
|
||||
}
|
||||
};
|
||||
const viewItems = views.getViews(ViewLocations.sqllab.rightSidebar) || [];
|
||||
const viewItems = useViews(ViewLocations.sqllab.rightSidebar) || [];
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
|
||||
@@ -31,7 +31,7 @@ import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { SqlLabRootState } from 'src/SqlLab/types';
|
||||
import { ViewLocations } from 'src/SqlLab/contributions';
|
||||
import PanelToolbar from 'src/components/PanelToolbar';
|
||||
import { views } from 'src/core';
|
||||
import { useViews } from 'src/core';
|
||||
import { resolveView } from 'src/core/views';
|
||||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||
import useLogAction from 'src/logger/useLogAction';
|
||||
@@ -107,7 +107,7 @@ const SouthPane = ({
|
||||
const editorId = tabViewId ?? id;
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
const viewItems = views.getViews(ViewLocations.sqllab.panels) || [];
|
||||
const viewItems = useViews(ViewLocations.sqllab.panels) || [];
|
||||
const { offline, tables } = useSelector(
|
||||
({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
|
||||
offline,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Flex } from '@superset-ui/core/components';
|
||||
import ViewListExtension from 'src/components/ViewListExtension';
|
||||
import { views } from 'src/core';
|
||||
import { useViews } from 'src/core';
|
||||
import { SQL_EDITOR_STATUSBAR_HEIGHT } from 'src/SqlLab/constants';
|
||||
import { ViewLocations } from 'src/SqlLab/contributions';
|
||||
|
||||
@@ -38,7 +38,7 @@ const Container = styled(Flex)`
|
||||
`;
|
||||
|
||||
const StatusBar = () => {
|
||||
const statusBarViews = views.getViews(ViewLocations.sqllab.statusBar) || [];
|
||||
const statusBarViews = useViews(ViewLocations.sqllab.statusBar) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import { useMenu } from 'src/core';
|
||||
import { css, useTheme } from '@apache-superset/core/theme';
|
||||
import { Button, Divider, Dropdown } from '@superset-ui/core/components';
|
||||
import { Menu, MenuItemType } from '@superset-ui/core/components/Menu';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { commands, menus } from 'src/core';
|
||||
import { commands } from 'src/core';
|
||||
|
||||
export interface PanelToolbarProps {
|
||||
viewId: string;
|
||||
@@ -35,7 +36,7 @@ const PanelToolbar = ({
|
||||
defaultSecondaryActions,
|
||||
}: PanelToolbarProps) => {
|
||||
const theme = useTheme();
|
||||
const menu = menus.getMenu(viewId);
|
||||
const menu = useMenu(viewId);
|
||||
|
||||
const primaryItems = menu?.primary || [];
|
||||
const secondaryItems = menu?.secondary || [];
|
||||
|
||||
@@ -39,8 +39,7 @@ jest.mock('./EditorProviders', () => ({
|
||||
getInstance: () => ({
|
||||
getProvider: jest.fn().mockReturnValue(undefined),
|
||||
hasProvider: jest.fn().mockReturnValue(false),
|
||||
onDidRegister: jest.fn().mockReturnValue({ dispose: jest.fn() }),
|
||||
onDidUnregister: jest.fn().mockReturnValue({ dispose: jest.fn() }),
|
||||
subscribe: jest.fn().mockReturnValue(() => {}),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -26,13 +26,12 @@
|
||||
* back to the default Ace editor.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, forwardRef } from 'react';
|
||||
import { useSyncExternalStore, forwardRef } from 'react';
|
||||
import type { editors } from '@apache-superset/core';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
import EditorProviders from './EditorProviders';
|
||||
import AceEditorProvider from './AceEditorProvider';
|
||||
|
||||
type EditorLanguage = editors.EditorLanguage;
|
||||
type EditorProps = editors.EditorProps;
|
||||
type EditorHandle = editors.EditorHandle;
|
||||
|
||||
@@ -42,49 +41,6 @@ type EditorHandle = editors.EditorHandle;
|
||||
*/
|
||||
export type EditorHostProps = EditorProps;
|
||||
|
||||
/**
|
||||
* Hook to track editor provider changes.
|
||||
* Returns the provider for the specified language and re-renders when it changes.
|
||||
*/
|
||||
const useEditorProvider = (language: EditorLanguage) => {
|
||||
const manager = EditorProviders.getInstance();
|
||||
const [provider, setProvider] = useState(() => manager.getProvider(language));
|
||||
|
||||
useEffect(() => {
|
||||
// Helper to safely update provider state, always fetching latest from manager
|
||||
const updateProvider = () => {
|
||||
setProvider(prev => {
|
||||
const current = manager.getProvider(language);
|
||||
return current !== prev ? current : prev;
|
||||
});
|
||||
};
|
||||
|
||||
// Subscribe to provider changes
|
||||
const registerDisposable = manager.onDidRegister(event => {
|
||||
if (event.editor.languages.includes(language)) {
|
||||
updateProvider();
|
||||
}
|
||||
});
|
||||
|
||||
const unregisterDisposable = manager.onDidUnregister(event => {
|
||||
if (event.editor.languages.includes(language)) {
|
||||
updateProvider();
|
||||
}
|
||||
});
|
||||
|
||||
// Check for provider on mount (in case it was registered before this component mounted)
|
||||
updateProvider();
|
||||
|
||||
return () => {
|
||||
registerDisposable.dispose();
|
||||
unregisterDisposable.dispose();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [language, manager]);
|
||||
|
||||
return provider;
|
||||
};
|
||||
|
||||
/**
|
||||
* EditorHost component that dynamically resolves and renders the appropriate editor.
|
||||
*
|
||||
@@ -106,7 +62,12 @@ const useEditorProvider = (language: EditorLanguage) => {
|
||||
const EditorHost = forwardRef<EditorHandle, EditorHostProps>((props, ref) => {
|
||||
const { language } = props;
|
||||
const theme = useTheme();
|
||||
const provider = useEditorProvider(language);
|
||||
const manager = EditorProviders.getInstance();
|
||||
const provider = useSyncExternalStore(
|
||||
manager.subscribe,
|
||||
() => manager.getProvider(language),
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
// Merge theme into props
|
||||
const propsWithTheme = { ...props, theme };
|
||||
|
||||
@@ -93,6 +93,17 @@ class EditorProviders {
|
||||
*/
|
||||
private unregisterEmitter = new EventEmitter<EditorUnregisteredEvent>();
|
||||
|
||||
private syncListeners: Set<() => void> = new Set();
|
||||
|
||||
/**
|
||||
* Stable-reference subscribe function for useSyncExternalStore.
|
||||
* Defined as an arrow property so the reference is bound to this instance at construction.
|
||||
*/
|
||||
public subscribe = (listener: () => void): (() => void) => {
|
||||
this.syncListeners.add(listener);
|
||||
return () => this.syncListeners.delete(listener);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
private constructor() {
|
||||
// Private constructor for singleton pattern
|
||||
@@ -145,6 +156,7 @@ class EditorProviders {
|
||||
|
||||
// Fire registration event
|
||||
this.registerEmitter.fire({ editor });
|
||||
this.syncListeners.forEach(l => l());
|
||||
|
||||
// Return disposable for cleanup
|
||||
return new Disposable(() => {
|
||||
@@ -176,6 +188,7 @@ class EditorProviders {
|
||||
|
||||
// Fire unregistration event
|
||||
this.unregisterEmitter.fire({ editor });
|
||||
this.syncListeners.forEach(l => l());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,6 +247,7 @@ class EditorProviders {
|
||||
public reset(): void {
|
||||
this.providers.clear();
|
||||
this.languageToProvider.clear();
|
||||
this.syncListeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
* and resolution functions declared in the API types.
|
||||
*/
|
||||
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import { editors as editorsApi } from '@apache-superset/core';
|
||||
import { Disposable } from '../models';
|
||||
import EditorProviders from './EditorProviders';
|
||||
@@ -109,6 +110,23 @@ export const onDidUnregisterEditor = (
|
||||
return manager.onDidUnregister(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook that returns the editor provider for a specific language and re-renders when it changes.
|
||||
*
|
||||
* @param language The language to get an editor for
|
||||
* @returns The editor provider or undefined if no extension provides one
|
||||
*/
|
||||
export const useEditor = (
|
||||
language: EditorLanguage,
|
||||
): EditorProvider | undefined => {
|
||||
const manager = EditorProviders.getInstance();
|
||||
return useSyncExternalStore(
|
||||
manager.subscribe,
|
||||
() => manager.getProvider(language),
|
||||
() => undefined,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Editors API object for use in the extension system.
|
||||
*/
|
||||
|
||||
@@ -24,11 +24,14 @@
|
||||
* Extensions register menu items as side effects at import time.
|
||||
*/
|
||||
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import type { menus as menusApi } from '@apache-superset/core';
|
||||
import { Disposable } from '../models';
|
||||
|
||||
type MenuItem = menusApi.MenuItem;
|
||||
type Menu = menusApi.Menu;
|
||||
type MenuItemRegisteredEvent = menusApi.MenuItemRegisteredEvent;
|
||||
type MenuItemUnregisteredEvent = menusApi.MenuItemUnregisteredEvent;
|
||||
|
||||
type StoredMenuItem = {
|
||||
item: MenuItem;
|
||||
@@ -38,6 +41,27 @@ type StoredMenuItem = {
|
||||
|
||||
const menuItems: StoredMenuItem[] = [];
|
||||
|
||||
const syncListeners = new Set<() => void>();
|
||||
const subscribe = (listener: () => void) => {
|
||||
syncListeners.add(listener);
|
||||
return () => syncListeners.delete(listener);
|
||||
};
|
||||
|
||||
const registerListeners = new Set<(e: MenuItemRegisteredEvent) => void>();
|
||||
const unregisterListeners = new Set<(e: MenuItemUnregisteredEvent) => void>();
|
||||
|
||||
const menuCache = new Map<string, Menu | undefined>();
|
||||
const notifyRegister = (event: MenuItemRegisteredEvent) => {
|
||||
menuCache.clear();
|
||||
syncListeners.forEach(l => l());
|
||||
registerListeners.forEach(l => l(event));
|
||||
};
|
||||
const notifyUnregister = (event: MenuItemUnregisteredEvent) => {
|
||||
menuCache.clear();
|
||||
syncListeners.forEach(l => l());
|
||||
unregisterListeners.forEach(l => l(event));
|
||||
};
|
||||
|
||||
const registerMenuItem: typeof menusApi.registerMenuItem = (
|
||||
item: MenuItem,
|
||||
location: string,
|
||||
@@ -45,11 +69,13 @@ const registerMenuItem: typeof menusApi.registerMenuItem = (
|
||||
): Disposable => {
|
||||
const stored: StoredMenuItem = { item, location, group };
|
||||
menuItems.push(stored);
|
||||
notifyRegister({ item, location, group });
|
||||
return new Disposable(() => {
|
||||
const index = menuItems.indexOf(stored);
|
||||
if (index >= 0) {
|
||||
menuItems.splice(index, 1);
|
||||
}
|
||||
notifyUnregister({ item, location, group });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -77,7 +103,34 @@ const getMenu: typeof menusApi.getMenu = (
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useMenu = (location: string): Menu | undefined =>
|
||||
useSyncExternalStore(
|
||||
subscribe,
|
||||
() => {
|
||||
if (!menuCache.has(location)) {
|
||||
menuCache.set(location, getMenu(location));
|
||||
}
|
||||
return menuCache.get(location);
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
export const onDidRegisterMenuItem: typeof menusApi.onDidRegisterMenuItem = (
|
||||
listener: (e: MenuItemRegisteredEvent) => void,
|
||||
): Disposable => {
|
||||
registerListeners.add(listener);
|
||||
return new Disposable(() => registerListeners.delete(listener));
|
||||
};
|
||||
|
||||
export const onDidUnregisterMenuItem: typeof menusApi.onDidUnregisterMenuItem =
|
||||
(listener: (e: MenuItemUnregisteredEvent) => void): Disposable => {
|
||||
unregisterListeners.add(listener);
|
||||
return new Disposable(() => unregisterListeners.delete(listener));
|
||||
};
|
||||
|
||||
export const menus: typeof menusApi = {
|
||||
registerMenuItem,
|
||||
getMenu,
|
||||
onDidRegisterMenuItem,
|
||||
onDidUnregisterMenuItem,
|
||||
};
|
||||
|
||||
@@ -24,13 +24,15 @@
|
||||
* Extensions register views as side effects at import time.
|
||||
*/
|
||||
|
||||
import React, { ReactElement } from 'react';
|
||||
import React, { ReactElement, useSyncExternalStore } from 'react';
|
||||
import type { views as viewsApi } from '@apache-superset/core';
|
||||
import { ErrorBoundary } from 'src/components/ErrorBoundary';
|
||||
import ExtensionPlaceholder from 'src/extensions/ExtensionPlaceholder';
|
||||
import { Disposable } from '../models';
|
||||
|
||||
type View = viewsApi.View;
|
||||
type ViewRegisteredEvent = viewsApi.ViewRegisteredEvent;
|
||||
type ViewUnregisteredEvent = viewsApi.ViewUnregisteredEvent;
|
||||
|
||||
const viewRegistry: Map<
|
||||
string,
|
||||
@@ -39,6 +41,27 @@ const viewRegistry: Map<
|
||||
|
||||
const locationIndex: Map<string, Set<string>> = new Map();
|
||||
|
||||
const syncListeners = new Set<() => void>();
|
||||
const subscribe = (listener: () => void) => {
|
||||
syncListeners.add(listener);
|
||||
return () => syncListeners.delete(listener);
|
||||
};
|
||||
|
||||
const registerListeners = new Set<(e: ViewRegisteredEvent) => void>();
|
||||
const unregisterListeners = new Set<(e: ViewUnregisteredEvent) => void>();
|
||||
|
||||
const viewsCache = new Map<string, View[] | undefined>();
|
||||
const notifyRegister = (event: ViewRegisteredEvent) => {
|
||||
viewsCache.clear();
|
||||
syncListeners.forEach(l => l());
|
||||
registerListeners.forEach(l => l(event));
|
||||
};
|
||||
const notifyUnregister = (event: ViewUnregisteredEvent) => {
|
||||
viewsCache.clear();
|
||||
syncListeners.forEach(l => l());
|
||||
unregisterListeners.forEach(l => l(event));
|
||||
};
|
||||
|
||||
const registerView: typeof viewsApi.registerView = (
|
||||
view: View,
|
||||
location: string,
|
||||
@@ -51,10 +74,12 @@ const registerView: typeof viewsApi.registerView = (
|
||||
const ids = locationIndex.get(location) ?? new Set();
|
||||
ids.add(id);
|
||||
locationIndex.set(location, ids);
|
||||
notifyRegister({ view, location });
|
||||
|
||||
return new Disposable(() => {
|
||||
viewRegistry.delete(id);
|
||||
locationIndex.get(location)?.delete(id);
|
||||
notifyUnregister({ view, location });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -77,7 +102,35 @@ const getViews: typeof viewsApi.getViews = (
|
||||
.filter((c): c is View => !!c);
|
||||
};
|
||||
|
||||
export const useViews = (location: string): View[] | undefined =>
|
||||
useSyncExternalStore(
|
||||
subscribe,
|
||||
() => {
|
||||
if (!viewsCache.has(location)) {
|
||||
viewsCache.set(location, getViews(location));
|
||||
}
|
||||
return viewsCache.get(location);
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
export const onDidRegisterView: typeof viewsApi.onDidRegisterView = (
|
||||
listener: (e: ViewRegisteredEvent) => void,
|
||||
): Disposable => {
|
||||
registerListeners.add(listener);
|
||||
return new Disposable(() => registerListeners.delete(listener));
|
||||
};
|
||||
|
||||
export const onDidUnregisterView: typeof viewsApi.onDidUnregisterView = (
|
||||
listener: (e: ViewUnregisteredEvent) => void,
|
||||
): Disposable => {
|
||||
unregisterListeners.add(listener);
|
||||
return new Disposable(() => unregisterListeners.delete(listener));
|
||||
};
|
||||
|
||||
export const views: typeof viewsApi = {
|
||||
registerView,
|
||||
getViews,
|
||||
onDidRegisterView,
|
||||
onDidUnregisterView,
|
||||
};
|
||||
|
||||
@@ -960,3 +960,92 @@ test('Clicking the gear "Add or edit filters and controls" item opens the Filter
|
||||
|
||||
expect(await screen.findByTestId('filter-modal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('FilterBar with orientation=Horizontal routes to Horizontal layout instead of Vertical', async () => {
|
||||
// Migrated from the disabled Cypress spec _skip.horizontalFilterBar.test.ts:
|
||||
// proves the orientation prop selects the Horizontal subtree. The settings
|
||||
// gear (FilterBarSettings) is rendered only by Horizontal.tsx — Vertical.tsx
|
||||
// does not mount it — so its presence is a horizontal-exclusive positive
|
||||
// signal that won't false-pass if vertical heading copy is tuned. We flush
|
||||
// all pending fake timers to clear useInitialization's setTimeout
|
||||
// regardless of the production timeout literal.
|
||||
const filter = createFilter({
|
||||
id: 'NATIVE_FILTER-h1',
|
||||
name: 'Horizontal filter',
|
||||
});
|
||||
const dataMask = createDataMask(filter.id);
|
||||
const state = createStateWithFilter(filter, dataMask, {
|
||||
filterBarOrientation: FilterBarOrientation.Horizontal,
|
||||
});
|
||||
|
||||
render(<FilterBar orientation={FilterBarOrientation.Horizontal} />, {
|
||||
initialState: state,
|
||||
useDnd: true,
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(screen.getByRole('img', { name: 'setting' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('FilterBar with orientation=Horizontal and no filters shows empty state alongside default actions', async () => {
|
||||
// Covers the second half of sc-107387 task #107390 ("show all default
|
||||
// actions in horizontal mode"). The original Cypress spec asserted four
|
||||
// affordances render when the bar is horizontal with no filters: the
|
||||
// empty-state copy, the settings gear, the action-buttons block, and the
|
||||
// create-filter entry inside the gear menu. The dropdown contents are
|
||||
// already covered by FilterBarSettings.test.tsx; here we keep scope to
|
||||
// the layout-level affordances that are exclusive to Horizontal.tsx.
|
||||
// Reload-persistence (the rest of #107390) is out of RTL scope and stays
|
||||
// queued for Playwright.
|
||||
const state = {
|
||||
...stateWithoutNativeFilters,
|
||||
dashboardInfo: {
|
||||
id: 1,
|
||||
dash_edit_perm: true,
|
||||
metadata: {
|
||||
native_filter_configuration: [],
|
||||
filterBarOrientation: FilterBarOrientation.Horizontal,
|
||||
},
|
||||
},
|
||||
dashboardState: {
|
||||
...stateWithoutNativeFilters.dashboardState,
|
||||
activeTabs: ['ROOT_ID'],
|
||||
},
|
||||
nativeFilters: { filters: {}, filtersState: {} },
|
||||
};
|
||||
|
||||
render(<FilterBar orientation={FilterBarOrientation.Horizontal} />, {
|
||||
initialState: state,
|
||||
useDnd: true,
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('horizontal-filterbar-empty')).toHaveTextContent(
|
||||
'No filters are currently added to this dashboard.',
|
||||
);
|
||||
expect(screen.getByRole('img', { name: 'setting' })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('filterbar-action-buttons')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('FilterBar with orientation=Vertical renders Vertical layout (sanity counterpart to the horizontal routing test)', () => {
|
||||
// Paired control for the routing test above: with Vertical orientation,
|
||||
// the settings gear must NOT be present (Vertical.tsx does not render
|
||||
// FilterBarSettings). Confirms the routing signal is horizontal-exclusive,
|
||||
// not a coincidence of when timers fire.
|
||||
const props = createClosedBarProps();
|
||||
renderFilterBar(props);
|
||||
expect(screen.getByText('Filters and controls')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('img', { name: 'setting' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* 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 { Preset } from '@superset-ui/core';
|
||||
import type { DataMaskStateWithId } from '@superset-ui/core';
|
||||
import type {
|
||||
DropdownContainerProps,
|
||||
DropdownItem,
|
||||
} from '@superset-ui/core/components/DropdownContainer';
|
||||
import { SelectFilterPlugin } from 'src/filters/components';
|
||||
import { FilterBarOrientation } from 'src/dashboard/types';
|
||||
import { act, render, waitFor, within } from 'spec/helpers/testing-library';
|
||||
import { createSelectNativeFilter } from 'spec/fixtures/mockNativeFilters';
|
||||
import FilterControls from './FilterControls';
|
||||
|
||||
// Capture every props snapshot DropdownContainer receives, plus the latest
|
||||
// onOverflowingStateChange callback. Tests drive overflow by invoking the
|
||||
// callback and then assert against the *next* captured props snapshot —
|
||||
// these are the values FilterControls itself computed (dropdownTriggerCount,
|
||||
// dropdownContent, items) so assertions exercise real production logic
|
||||
// rather than props the test handed in directly.
|
||||
const dropdownContainerProps: DropdownContainerProps[] = [];
|
||||
const callbackRef: {
|
||||
current:
|
||||
| ((s: { overflowed: string[]; notOverflowed: string[] }) => void)
|
||||
| null;
|
||||
} = { current: null };
|
||||
|
||||
// Mock the DropdownContainer subpath rather than the barrel
|
||||
// `@superset-ui/core/components` — mocking the barrel triggers a
|
||||
// circular re-export chain at requireActual time
|
||||
// (LabeledErrorBoundInput → ActionButton is undefined at that point).
|
||||
// The barrel's `export { DropdownContainer } from './DropdownContainer'`
|
||||
// resolves to this subpath, so the mock is picked up transparently.
|
||||
jest.mock('@superset-ui/core/components/DropdownContainer', () => {
|
||||
const React = jest.requireActual('react');
|
||||
const MockDropdownContainer = React.forwardRef(
|
||||
(props: DropdownContainerProps, ref: React.Ref<unknown>) => {
|
||||
dropdownContainerProps.push(props);
|
||||
callbackRef.current = props.onOverflowingStateChange ?? null;
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
open: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
return (
|
||||
<div data-test="dropdown-container-mock">
|
||||
<div data-test="dropdown-items">
|
||||
{props.items.map((item: DropdownItem) => (
|
||||
<div key={item.id} data-test="dropdown-item">
|
||||
{item.element}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div data-test="dropdown-trigger-text">
|
||||
{props.dropdownTriggerText}
|
||||
</div>
|
||||
<div data-test="dropdown-trigger-count">
|
||||
{props.dropdownTriggerCount}
|
||||
</div>
|
||||
{props.dropdownContent && (
|
||||
<div data-test="dropdown-content-mock">
|
||||
{props.dropdownContent([])}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
return { __esModule: true, DropdownContainer: MockDropdownContainer };
|
||||
});
|
||||
|
||||
class OverflowTestPreset extends Preset {
|
||||
constructor() {
|
||||
super({
|
||||
name: 'FilterControls overflow test preset',
|
||||
plugins: [new SelectFilterPlugin().configure({ key: 'filter_select' })],
|
||||
});
|
||||
}
|
||||
}
|
||||
new OverflowTestPreset().register();
|
||||
|
||||
// Tabless dashboard layout ⇒ useSelectFiltersInScope returns all filters in
|
||||
// scope without needing to model tab parentage.
|
||||
const buildHorizontalState = (
|
||||
filters: ReturnType<typeof createSelectNativeFilter>[],
|
||||
) => ({
|
||||
dashboardInfo: {
|
||||
id: 1,
|
||||
dash_edit_perm: true,
|
||||
filterBarOrientation: FilterBarOrientation.Horizontal,
|
||||
metadata: {
|
||||
native_filter_configuration: filters,
|
||||
},
|
||||
},
|
||||
dashboardLayout: {
|
||||
present: {
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: [] },
|
||||
},
|
||||
past: [],
|
||||
future: [],
|
||||
},
|
||||
dashboardState: {
|
||||
sliceIds: [],
|
||||
activeTabs: ['ROOT_ID'],
|
||||
},
|
||||
charts: {},
|
||||
nativeFilters: {
|
||||
filters: filters.reduce(
|
||||
(acc, f) => ({ ...acc, [f.id]: f }),
|
||||
{} as Record<string, ReturnType<typeof createSelectNativeFilter>>,
|
||||
),
|
||||
filtersState: {},
|
||||
},
|
||||
dataMask: {},
|
||||
sliceEntities: { slices: {} },
|
||||
datasources: {},
|
||||
});
|
||||
|
||||
const buildDataMaskSelected = (
|
||||
filters: ReturnType<typeof createSelectNativeFilter>[],
|
||||
withValueIds: string[] = [],
|
||||
): DataMaskStateWithId =>
|
||||
filters.reduce(
|
||||
(acc, f) => ({
|
||||
...acc,
|
||||
[f.id]: {
|
||||
id: f.id,
|
||||
filterState: {
|
||||
value: withValueIds.includes(f.id) ? ['set'] : null,
|
||||
},
|
||||
extraFormData: {},
|
||||
},
|
||||
}),
|
||||
{} as DataMaskStateWithId,
|
||||
);
|
||||
|
||||
const renderHorizontal = (
|
||||
filters: ReturnType<typeof createSelectNativeFilter>[],
|
||||
dataMaskSelected: DataMaskStateWithId,
|
||||
) =>
|
||||
render(
|
||||
<FilterControls
|
||||
dataMaskSelected={dataMaskSelected}
|
||||
onFilterSelectionChange={jest.fn()}
|
||||
onPendingCustomizationDataMaskChange={jest.fn()}
|
||||
chartCustomizationValues={[]}
|
||||
/>,
|
||||
{
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: buildHorizontalState(filters),
|
||||
},
|
||||
);
|
||||
|
||||
const latestProps = () =>
|
||||
dropdownContainerProps[dropdownContainerProps.length - 1];
|
||||
|
||||
const fireOverflow = (overflowed: string[], notOverflowed: string[]) => {
|
||||
if (!callbackRef.current) {
|
||||
throw new Error('onOverflowingStateChange callback not captured');
|
||||
}
|
||||
act(() => {
|
||||
callbackRef.current!({ overflowed, notOverflowed });
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
dropdownContainerProps.length = 0;
|
||||
callbackRef.current = null;
|
||||
});
|
||||
|
||||
test('horizontal FilterControls hands every filter to DropdownContainer as an item', async () => {
|
||||
const filters = [
|
||||
createSelectNativeFilter('NATIVE_FILTER-1', 'country'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-2', 'region'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-3', 'city'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-4', 'zip'),
|
||||
];
|
||||
|
||||
renderHorizontal(filters, buildDataMaskSelected(filters));
|
||||
|
||||
await waitFor(() => expect(latestProps()).toBeTruthy());
|
||||
|
||||
expect(latestProps().items.map((i: DropdownItem) => i.id)).toEqual([
|
||||
'NATIVE_FILTER-1',
|
||||
'NATIVE_FILTER-2',
|
||||
'NATIVE_FILTER-3',
|
||||
'NATIVE_FILTER-4',
|
||||
]);
|
||||
// dropdownTriggerText is the production string FilterControls passes in.
|
||||
expect(latestProps().dropdownTriggerText).toBe('More filters');
|
||||
});
|
||||
|
||||
test('with no overflow callback fired, dropdown trigger count is 0 and content is empty', async () => {
|
||||
const filters = [
|
||||
createSelectNativeFilter('NATIVE_FILTER-1', 'country'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-2', 'region'),
|
||||
];
|
||||
|
||||
renderHorizontal(
|
||||
filters,
|
||||
buildDataMaskSelected(filters, ['NATIVE_FILTER-1']),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(latestProps()).toBeTruthy());
|
||||
|
||||
expect(latestProps().dropdownTriggerCount).toBe(0);
|
||||
// FilterControls only supplies dropdownContent when something overflowed.
|
||||
expect(latestProps().dropdownContent).toBeUndefined();
|
||||
});
|
||||
|
||||
test('firing overflow with two filters that have values increments the trigger count to 2', async () => {
|
||||
const filters = [
|
||||
createSelectNativeFilter('NATIVE_FILTER-1', 'country'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-2', 'region'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-3', 'city'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-4', 'zip'),
|
||||
];
|
||||
|
||||
renderHorizontal(
|
||||
filters,
|
||||
// Mark the two we plan to overflow as having values; the production
|
||||
// selector activeOverflowedFiltersInScope filters on dataMask.filterState.value.
|
||||
buildDataMaskSelected(filters, ['NATIVE_FILTER-3', 'NATIVE_FILTER-4']),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(callbackRef.current).toBeTruthy());
|
||||
|
||||
fireOverflow(
|
||||
['NATIVE_FILTER-3', 'NATIVE_FILTER-4'],
|
||||
['NATIVE_FILTER-1', 'NATIVE_FILTER-2'],
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(latestProps().dropdownTriggerCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
test('firing overflow with no active values keeps trigger count at 0 but supplies dropdownContent', async () => {
|
||||
// Reinforces the activeOverflowedFiltersInScope branch in
|
||||
// FilterControls.tsx: count is the *active* (value-bearing) subset of
|
||||
// overflowed filters, not the raw overflowed count. If the production
|
||||
// memo regressed to use overflowedFiltersInScope.length, this fails.
|
||||
const filters = [
|
||||
createSelectNativeFilter('NATIVE_FILTER-1', 'country'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-2', 'region'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-3', 'city'),
|
||||
];
|
||||
|
||||
renderHorizontal(filters, buildDataMaskSelected(filters));
|
||||
|
||||
await waitFor(() => expect(callbackRef.current).toBeTruthy());
|
||||
|
||||
fireOverflow(['NATIVE_FILTER-2', 'NATIVE_FILTER-3'], ['NATIVE_FILTER-1']);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(latestProps().dropdownContent).toBeInstanceOf(Function);
|
||||
});
|
||||
expect(latestProps().dropdownTriggerCount).toBe(0);
|
||||
});
|
||||
|
||||
test('all 12 overflowed filters are reachable through dropdownContent', async () => {
|
||||
// Substitutes for the disabled Cypress "scroll within overflow" assertion:
|
||||
// jsdom has no real layout/scrolling, so we instead prove every overflowed
|
||||
// filter renders inside the dropdown panel.
|
||||
const filters = Array.from({ length: 12 }, (_, i) =>
|
||||
createSelectNativeFilter(`NATIVE_FILTER-${i + 1}`, `filter_${i + 1}`),
|
||||
);
|
||||
|
||||
renderHorizontal(filters, buildDataMaskSelected(filters));
|
||||
|
||||
await waitFor(() => expect(callbackRef.current).toBeTruthy());
|
||||
|
||||
fireOverflow(
|
||||
filters.map(f => f.id),
|
||||
[],
|
||||
);
|
||||
|
||||
// dropdownContent renders FiltersDropdownContent, which renders each
|
||||
// overflowed filter through the renderer prop. Asserting on identity
|
||||
// (not just count) catches a regression that rendered the wrong subset
|
||||
// of filters in the dropdown — e.g. all `filtersInScope` instead of
|
||||
// the overflowed slice.
|
||||
const { findByTestId } = within(document.body);
|
||||
const contentSlot = await findByTestId('dropdown-content-mock');
|
||||
await waitFor(() => {
|
||||
const names = within(contentSlot).getAllByTestId('filter-control-name');
|
||||
expect(names.map(n => n.textContent)).toEqual(filters.map(f => f.name));
|
||||
});
|
||||
});
|
||||
@@ -16,9 +16,26 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { NativeFilterType } from '@superset-ui/core';
|
||||
import { NativeFilterType, Preset } from '@superset-ui/core';
|
||||
import type { Filter } from '@superset-ui/core';
|
||||
import { SelectFilterPlugin } from 'src/filters/components';
|
||||
import { FilterBarOrientation } from 'src/dashboard/types';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import { createSelectNativeFilter } from 'spec/fixtures/mockNativeFilters';
|
||||
import HorizontalBar from './Horizontal';
|
||||
import type { HorizontalBarProps } from './types';
|
||||
|
||||
// Register the select filter plugin once so FilterControl can render the
|
||||
// filter name without throwing when the plugin registry is consulted.
|
||||
class HorizontalFilterBarTestPreset extends Preset {
|
||||
constructor() {
|
||||
super({
|
||||
name: 'Horizontal filter bar test preset',
|
||||
plugins: [new SelectFilterPlugin().configure({ key: 'filter_select' })],
|
||||
});
|
||||
}
|
||||
}
|
||||
new HorizontalFilterBarTestPreset().register();
|
||||
|
||||
const defaultProps = {
|
||||
actions: null,
|
||||
@@ -32,7 +49,7 @@ const defaultProps = {
|
||||
onPendingCustomizationDataMaskChange: jest.fn(),
|
||||
};
|
||||
|
||||
const renderWrapper = (overrideProps?: Record<string, any>) =>
|
||||
const renderWrapper = (overrideProps?: Partial<HorizontalBarProps>) =>
|
||||
waitFor(() =>
|
||||
render(<HorizontalBar {...defaultProps} {...overrideProps} />, {
|
||||
useRedux: true,
|
||||
@@ -60,11 +77,13 @@ test('should render', async () => {
|
||||
|
||||
test('should not render the empty message', async () => {
|
||||
await renderWrapper({
|
||||
// Intentionally minimal — Horizontal only reads filterValues.length
|
||||
// here, so the missing required Filter fields would never be read.
|
||||
filterValues: [
|
||||
{
|
||||
id: 'test',
|
||||
type: NativeFilterType.NativeFilter,
|
||||
},
|
||||
} as unknown as Filter,
|
||||
],
|
||||
});
|
||||
expect(
|
||||
@@ -92,3 +111,133 @@ test('should render the loading icon', async () => {
|
||||
});
|
||||
expect(screen.getByRole('status', { name: 'Loading' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// --- Tests migrated from disabled Cypress spec
|
||||
// `_skip.horizontalFilterBar.test.ts` (sc-107387). ---
|
||||
|
||||
const buildStateWithFilters = (
|
||||
filters: ReturnType<typeof createSelectNativeFilter>[],
|
||||
) => ({
|
||||
dashboardState: {
|
||||
sliceIds: [],
|
||||
activeTabs: ['ROOT_ID'],
|
||||
},
|
||||
dashboardInfo: {
|
||||
id: 1,
|
||||
dash_edit_perm: true,
|
||||
filterBarOrientation: FilterBarOrientation.Horizontal,
|
||||
metadata: {
|
||||
native_filter_configuration: filters,
|
||||
},
|
||||
},
|
||||
dashboardLayout: {
|
||||
present: {
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: [] },
|
||||
},
|
||||
past: [],
|
||||
future: [],
|
||||
},
|
||||
charts: {},
|
||||
nativeFilters: {
|
||||
filters: filters.reduce(
|
||||
(acc, f) => ({ ...acc, [f.id]: f }),
|
||||
{} as Record<string, ReturnType<typeof createSelectNativeFilter>>,
|
||||
),
|
||||
filtersState: {},
|
||||
},
|
||||
dataMask: filters.reduce(
|
||||
(acc, f) => ({
|
||||
...acc,
|
||||
[f.id]: { id: f.id, filterState: { value: null }, extraFormData: {} },
|
||||
}),
|
||||
{} as Record<string, unknown>,
|
||||
),
|
||||
sliceEntities: { slices: {} },
|
||||
datasources: {},
|
||||
});
|
||||
|
||||
const renderWithFilters = (
|
||||
filters: ReturnType<typeof createSelectNativeFilter>[],
|
||||
overrideProps?: Partial<HorizontalBarProps>,
|
||||
) =>
|
||||
render(<HorizontalBar {...defaultProps} {...overrideProps} />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: buildStateWithFilters(filters),
|
||||
});
|
||||
|
||||
test('renders default actions slot, settings gear, and empty message together in horizontal mode', async () => {
|
||||
const sentinelActions = (
|
||||
<button type="button" data-test="sentinel-actions">
|
||||
apply
|
||||
</button>
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
render(
|
||||
<HorizontalBar
|
||||
{...defaultProps}
|
||||
actions={sentinelActions}
|
||||
filterValues={[]}
|
||||
/>,
|
||||
{
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: {
|
||||
dashboardState: { sliceIds: [] },
|
||||
dashboardInfo: {
|
||||
id: 1,
|
||||
dash_edit_perm: true,
|
||||
filterBarOrientation: FilterBarOrientation.Horizontal,
|
||||
},
|
||||
dashboardLayout: { present: {}, past: [], future: [] },
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('No filters are currently added to this dashboard.'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('img', { name: 'setting' })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sentinel-actions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders all native filters supplied via filterValues in horizontal mode', async () => {
|
||||
const filters = [
|
||||
createSelectNativeFilter('NATIVE_FILTER-1', 'test_1', 'country_name'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-2', 'test_2', 'country_code'),
|
||||
createSelectNativeFilter('NATIVE_FILTER-3', 'test_3', 'region'),
|
||||
];
|
||||
|
||||
renderWithFilters(filters, { filterValues: filters });
|
||||
|
||||
await waitFor(() => {
|
||||
const filterNames = screen.getAllByTestId('filter-control-name');
|
||||
expect(filterNames).toHaveLength(3);
|
||||
});
|
||||
|
||||
['test_1', 'test_2', 'test_3'].forEach(name => {
|
||||
expect(screen.getByText(name)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('omits the empty message when at least one filter value is supplied', async () => {
|
||||
// Companion to the "renders all native filters" test above: the migrated
|
||||
// Cypress "display newly added filter" scenario reduces, at this layer, to
|
||||
// proving that supplying a filter value flips off the empty state. The
|
||||
// upstream user flow (open edit modal, add filter, save) is integration
|
||||
// territory and not covered here.
|
||||
const filters = [
|
||||
createSelectNativeFilter('NATIVE_FILTER-1', 'just_added', 'country_name'),
|
||||
];
|
||||
|
||||
renderWithFilters(filters, { filterValues: filters });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('just_added')).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.queryByText('No filters are currently added to this dashboard.'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
import * as supersetCore from '@apache-superset/core';
|
||||
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
|
||||
@@ -52,20 +52,12 @@ declare global {
|
||||
const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
const userId = useSelector<RootState, number | undefined>(
|
||||
({ user }) => user.userId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialized) return;
|
||||
|
||||
if (!userId) {
|
||||
// No user logged in — nothing to initialize
|
||||
setInitialized(true);
|
||||
return;
|
||||
}
|
||||
if (userId == null) return;
|
||||
|
||||
// Provide the implementations for @apache-superset/core
|
||||
window.superset = {
|
||||
@@ -80,19 +72,10 @@ const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
|
||||
views,
|
||||
};
|
||||
|
||||
const setup = async () => {
|
||||
if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
|
||||
await ExtensionsLoader.getInstance().initializeExtensions();
|
||||
}
|
||||
setInitialized(true);
|
||||
};
|
||||
|
||||
setup();
|
||||
}, [initialized, userId]);
|
||||
|
||||
if (!initialized) {
|
||||
return null;
|
||||
}
|
||||
if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
|
||||
ExtensionsLoader.getInstance().initializeExtensions();
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
2
superset-websocket/package-lock.json
generated
2
superset-websocket/package-lock.json
generated
@@ -25,7 +25,7 @@
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||
"@typescript-eslint/parser": "^8.60.1",
|
||||
"eslint": "^10.4.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||
"@typescript-eslint/parser": "^8.60.1",
|
||||
"eslint": "^10.4.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
|
||||
118
superset-websocket/utils/client-ws-app/package-lock.json
generated
118
superset-websocket/utils/client-ws-app/package-lock.json
generated
@@ -13,7 +13,7 @@
|
||||
"express": "~5.2.1",
|
||||
"http-errors": "~2.0.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"morgan": "~1.10.1",
|
||||
"morgan": "~1.11.0",
|
||||
"pug": "~3.0.4"
|
||||
}
|
||||
},
|
||||
@@ -127,17 +127,6 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
@@ -455,17 +444,6 @@
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||
@@ -483,18 +461,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler/node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -867,18 +833,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/morgan": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz",
|
||||
"integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==",
|
||||
"dependencies": {
|
||||
"basic-auth": "~2.0.1",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-finished": "~2.3.0",
|
||||
"on-finished": "~2.4.1",
|
||||
"on-headers": "~1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/morgan/node_modules/debug": {
|
||||
@@ -928,9 +898,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
@@ -1227,18 +1197,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
||||
@@ -1509,16 +1467,6 @@
|
||||
"qs": "^6.14.0",
|
||||
"raw-body": "^3.0.1",
|
||||
"type-is": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"buffer-equal-constant-time": {
|
||||
@@ -1740,14 +1688,6 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1762,16 +1702,6 @@
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"forwarded": {
|
||||
@@ -2040,14 +1970,14 @@
|
||||
}
|
||||
},
|
||||
"morgan": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz",
|
||||
"integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==",
|
||||
"requires": {
|
||||
"basic-auth": "~2.0.1",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-finished": "~2.3.0",
|
||||
"on-finished": "~2.4.1",
|
||||
"on-headers": "~1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -2087,9 +2017,9 @@
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
@@ -2337,16 +2267,6 @@
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"express": "~5.2.1",
|
||||
"http-errors": "~2.0.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"morgan": "~1.10.1",
|
||||
"morgan": "~1.11.0",
|
||||
"pug": "~3.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,19 @@ class DatasourceTypeUpdateRequiredValidationError(ValidationError):
|
||||
)
|
||||
|
||||
|
||||
class ChartQueryContextDatasourceMismatchValidationError(ValidationError):
|
||||
"""
|
||||
Raised when a query-context-only update carries a datasource that does not
|
||||
match the chart's own datasource.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
_("The query context datasource does not match the chart datasource"),
|
||||
field_name="query_context",
|
||||
)
|
||||
|
||||
|
||||
class ChartNotFoundError(CommandException):
|
||||
message = "Chart not found."
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from superset.commands.chart.exceptions import (
|
||||
ChartForbiddenError,
|
||||
ChartInvalidError,
|
||||
ChartNotFoundError,
|
||||
ChartQueryContextDatasourceMismatchValidationError,
|
||||
ChartUpdateFailedError,
|
||||
DashboardsForbiddenError,
|
||||
DashboardsNotFoundValidationError,
|
||||
@@ -41,6 +42,7 @@ from superset.exceptions import SupersetSecurityException
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.slice import Slice
|
||||
from superset.tags.models import ObjectType
|
||||
from superset.utils import json
|
||||
from superset.utils.decorators import on_error, transaction
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -101,6 +103,51 @@ class UpdateChartCommand(UpdateMixin, BaseCommand):
|
||||
if not security_manager.is_owner(dash):
|
||||
raise DashboardsForbiddenError()
|
||||
|
||||
def _validate_query_context_datasource(
|
||||
self, exceptions: list[ValidationError]
|
||||
) -> None:
|
||||
"""
|
||||
Ensure a query-context-only update keeps the chart's own datasource.
|
||||
|
||||
The submitted query context is only verified when it carries a parseable
|
||||
``datasource`` object; a payload that references a different datasource than
|
||||
the chart's persisted one is rejected. Payloads without a datasource fall
|
||||
back to the chart's datasource at execution time and need no check.
|
||||
"""
|
||||
if not self._model:
|
||||
return
|
||||
|
||||
raw_query_context = self._properties.get("query_context")
|
||||
if not raw_query_context:
|
||||
return
|
||||
|
||||
try:
|
||||
query_context = json.loads(raw_query_context)
|
||||
except (TypeError, ValueError):
|
||||
# An unparseable payload cannot be verified or replayed; leave it for
|
||||
# downstream handling rather than guessing at its intent.
|
||||
return
|
||||
|
||||
datasource = (
|
||||
query_context.get("datasource") if isinstance(query_context, dict) else None
|
||||
)
|
||||
if not isinstance(datasource, dict):
|
||||
return
|
||||
|
||||
try:
|
||||
ids_match = int(datasource["id"]) == self._model.datasource_id
|
||||
except (KeyError, TypeError, ValueError):
|
||||
ids_match = False
|
||||
|
||||
datasource_type = datasource.get("type")
|
||||
types_match = (
|
||||
datasource_type is None
|
||||
or str(datasource_type) == self._model.datasource_type
|
||||
)
|
||||
|
||||
if not ids_match or not types_match:
|
||||
exceptions.append(ChartQueryContextDatasourceMismatchValidationError())
|
||||
|
||||
def validate(self) -> None: # noqa: C901
|
||||
exceptions: list[ValidationError] = []
|
||||
dashboard_ids = self._properties.get("dashboards")
|
||||
@@ -134,6 +181,12 @@ class UpdateChartCommand(UpdateMixin, BaseCommand):
|
||||
raise ChartForbiddenError() from ex
|
||||
except ValidationError as ex:
|
||||
exceptions.append(ex)
|
||||
else:
|
||||
# The query-context-only path skips the ownership check so report and
|
||||
# alert workers can refresh a chart's cached payload. Keep that payload
|
||||
# bound to the chart's own datasource so it cannot be repointed at an
|
||||
# unrelated one.
|
||||
self._validate_query_context_datasource(exceptions)
|
||||
|
||||
# validate tags
|
||||
try:
|
||||
|
||||
@@ -459,6 +459,7 @@ LANGUAGES = {
|
||||
"nl": {"flag": "nl", "name": "Dutch"},
|
||||
"uk": {"flag": "ua", "name": "Ukrainian"},
|
||||
"mi": {"flag": "nz", "name": "Māori"},
|
||||
"ro": {"flag": "ro", "name": "Romanian"},
|
||||
}
|
||||
# Turning off i18n by default as translation in most languages are
|
||||
# incomplete and not well maintained.
|
||||
|
||||
16267
superset/translations/ro/LC_MESSAGES/messages.po
Normal file
16267
superset/translations/ro/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ import pandas as pd
|
||||
import simplejson
|
||||
from flask_babel.speaklater import LazyString
|
||||
from jsonpath_ng import parse
|
||||
from jsonpath_ng.jsonpath import Child, Fields, Root
|
||||
from simplejson import JSONDecodeError
|
||||
|
||||
from superset.constants import PASSWORD_MASK
|
||||
@@ -304,6 +305,30 @@ def reveal_sensitive(
|
||||
return revealed_payload
|
||||
|
||||
|
||||
def _render_jsonpath(node: Any) -> str:
|
||||
"""
|
||||
Render a JSONPath node as a stable dotted string (e.g. ``foo.bar``).
|
||||
|
||||
``str()`` of a jsonpath-ng path is not stable across releases: as of
|
||||
jsonpath-ng 1.8.0 a ``Child`` node renders with surrounding parentheses
|
||||
(e.g. ``(foo.bar)``). This helper produces the historic dotted notation so
|
||||
that the strings returned by :func:`get_masked_fields` remain consistent and
|
||||
can be round-tripped back through ``parse``.
|
||||
|
||||
Falls back to ``str()`` for node kinds that aren't plain field access (e.g.
|
||||
array indices, slices), preserving their existing representation.
|
||||
"""
|
||||
if isinstance(node, Child):
|
||||
left = _render_jsonpath(node.left)
|
||||
right = _render_jsonpath(node.right)
|
||||
return f"{left}.{right}" if left else right
|
||||
if isinstance(node, Fields):
|
||||
return ".".join(node.fields)
|
||||
if isinstance(node, Root):
|
||||
return ""
|
||||
return str(node)
|
||||
|
||||
|
||||
def get_masked_fields(
|
||||
payload: dict[str, Any],
|
||||
sensitive_fields: set[str],
|
||||
@@ -321,8 +346,10 @@ def get_masked_fields(
|
||||
for match in jsonpath_expr.find(payload):
|
||||
if match.value == PASSWORD_MASK:
|
||||
# Using `match.full_path` instead of json_path to account
|
||||
# for wildcards
|
||||
masked.append(f"$.{match.full_path}")
|
||||
# for wildcards. Render the path explicitly so the output is
|
||||
# stable across jsonpath-ng versions (newer releases wrap
|
||||
# `Child` paths in parentheses when stringified).
|
||||
masked.append(f"$.{_render_jsonpath(match.full_path)}")
|
||||
return masked
|
||||
|
||||
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from superset.commands.chart.exceptions import ChartForbiddenError
|
||||
from superset.commands.chart.exceptions import ChartForbiddenError, ChartInvalidError
|
||||
from superset.commands.chart.update import UpdateChartCommand
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.utils import json
|
||||
|
||||
|
||||
def _ownership_exc() -> SupersetSecurityException:
|
||||
@@ -91,3 +92,73 @@ def test_update_chart_owner_can_perform_regular_update(
|
||||
|
||||
find_by_id.assert_called_once_with(1)
|
||||
raise_for_ownership.assert_called_once()
|
||||
|
||||
|
||||
def _query_context_payload(datasource: object) -> dict[str, object]:
|
||||
return {
|
||||
"query_context": json.dumps({"datasource": datasource, "queries": []}),
|
||||
"query_context_generation": True,
|
||||
}
|
||||
|
||||
|
||||
def test_update_chart_query_context_matching_datasource_is_allowed(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""A query context that targets the chart's own datasource is accepted."""
|
||||
find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id")
|
||||
find_by_id.return_value = mocker.MagicMock(
|
||||
id=1, tags=[], dashboards=[], datasource_id=42, datasource_type="table"
|
||||
)
|
||||
mocker.patch("superset.commands.chart.update.security_manager.raise_for_ownership")
|
||||
|
||||
UpdateChartCommand(
|
||||
1, _query_context_payload({"id": 42, "type": "table"})
|
||||
).validate()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"datasource",
|
||||
[
|
||||
{"id": 99, "type": "table"}, # different id
|
||||
{"id": 42, "type": "query"}, # different type
|
||||
{"id": "99", "type": "table"}, # different id as string
|
||||
],
|
||||
)
|
||||
def test_update_chart_query_context_mismatched_datasource_is_rejected(
|
||||
mocker: MockerFixture,
|
||||
datasource: dict[str, object],
|
||||
) -> None:
|
||||
"""A query context pointing at a different datasource is rejected with a 4xx."""
|
||||
find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id")
|
||||
find_by_id.return_value = mocker.MagicMock(
|
||||
id=1, tags=[], dashboards=[], datasource_id=42, datasource_type="table"
|
||||
)
|
||||
mocker.patch("superset.commands.chart.update.security_manager.raise_for_ownership")
|
||||
|
||||
with pytest.raises(ChartInvalidError):
|
||||
UpdateChartCommand(1, _query_context_payload(datasource)).validate()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"query_context",
|
||||
[
|
||||
"{}", # no datasource key
|
||||
'{"datasource": null}', # null datasource
|
||||
"not-json", # unparseable payload
|
||||
],
|
||||
)
|
||||
def test_update_chart_query_context_without_datasource_is_allowed(
|
||||
mocker: MockerFixture,
|
||||
query_context: str,
|
||||
) -> None:
|
||||
"""Payloads with no verifiable datasource fall back to the chart's own."""
|
||||
find_by_id = mocker.patch("superset.commands.chart.update.ChartDAO.find_by_id")
|
||||
find_by_id.return_value = mocker.MagicMock(
|
||||
id=1, tags=[], dashboards=[], datasource_id=42, datasource_type="table"
|
||||
)
|
||||
mocker.patch("superset.commands.chart.update.security_manager.raise_for_ownership")
|
||||
|
||||
UpdateChartCommand(
|
||||
1,
|
||||
{"query_context": query_context, "query_context_generation": True},
|
||||
).validate()
|
||||
|
||||
81
tests/unit_tests/utils/test_split.py
Normal file
81
tests/unit_tests/utils/test_split.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from superset.utils.core import split
|
||||
|
||||
|
||||
def test_split_empty_string():
|
||||
assert list(split("")) == [""]
|
||||
|
||||
|
||||
def test_split_leading_delimiter():
|
||||
assert list(split(" a")) == [
|
||||
"",
|
||||
"a",
|
||||
]
|
||||
|
||||
|
||||
def test_split_trailing_delimiter():
|
||||
assert list(split("a ")) == [
|
||||
"a",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
def test_split_only_delimiter():
|
||||
assert list(split(" ")) == [
|
||||
"",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
def test_split_nested_parentheses():
|
||||
assert list(
|
||||
split(
|
||||
"a,(b,(c,d))",
|
||||
delimiter=",",
|
||||
)
|
||||
) == [
|
||||
"a",
|
||||
"(b,(c,d))",
|
||||
]
|
||||
|
||||
|
||||
def test_branch_separator_found():
|
||||
assert list(split("a b")) == [
|
||||
"a",
|
||||
"b",
|
||||
]
|
||||
|
||||
|
||||
def test_branch_separator_not_found():
|
||||
assert list(split("ab")) == [
|
||||
"ab",
|
||||
]
|
||||
|
||||
|
||||
def test_branch_parentheses():
|
||||
assert list(split("(a b)")) == [
|
||||
"(a b)",
|
||||
]
|
||||
|
||||
|
||||
def test_branch_escaped_quote():
|
||||
assert list(split(r'"a\"b c" d')) == [
|
||||
r'"a\"b c"',
|
||||
"d",
|
||||
]
|
||||
Reference in New Issue
Block a user