mirror of
https://github.com/apache/superset.git
synced 2026-06-11 18:49:15 +00:00
Compare commits
20 Commits
tanstack-r
...
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 }],
|
||||
|
||||
417
superset-frontend/package-lock.json
generated
417
superset-frontend/package-lock.json
generated
@@ -71,7 +71,6 @@
|
||||
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
||||
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
|
||||
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
||||
"@tanstack/react-router": "^1.170.15",
|
||||
"@types/d3-format": "^3.0.1",
|
||||
"@types/d3-selection": "^3.0.11",
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
@@ -96,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",
|
||||
@@ -136,6 +135,7 @@
|
||||
"react-redux": "^7.2.9",
|
||||
"react-resize-detector": "^9.1.1",
|
||||
"react-reverse-portal": "^2.3.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-search-input": "^0.11.3",
|
||||
"react-split": "^2.0.9",
|
||||
"react-table": "^7.8.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",
|
||||
@@ -206,6 +206,7 @@
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/redux-localstorage": "^1.0.8",
|
||||
@@ -241,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",
|
||||
@@ -271,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",
|
||||
@@ -3938,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": {
|
||||
@@ -9695,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"
|
||||
@@ -9715,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": {
|
||||
@@ -9723,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": {
|
||||
@@ -9774,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": {
|
||||
@@ -9786,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",
|
||||
@@ -9813,7 +9767,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.1"
|
||||
"storybook": "^10.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -9822,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": {
|
||||
@@ -9835,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": {
|
||||
@@ -9857,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",
|
||||
@@ -9880,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": {
|
||||
@@ -9889,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"
|
||||
},
|
||||
@@ -9909,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": {
|
||||
@@ -9989,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": {
|
||||
@@ -10003,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": {
|
||||
@@ -10015,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",
|
||||
@@ -10032,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": {
|
||||
@@ -10753,89 +10742,6 @@
|
||||
"@swc/counter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/history": {
|
||||
"version": "1.162.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.0.tgz",
|
||||
"integrity": "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-router": {
|
||||
"version": "1.170.15",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.15.tgz",
|
||||
"integrity": "sha512-GawYz7HEjj8rTUUDoT/SemDEVm63pZUO+2mOcXHY9Jl3EwMS5gFBnPu/2UvcrwRm1jN1k79fokc0d4aFmrLatg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.162.0",
|
||||
"@tanstack/react-store": "^0.9.3",
|
||||
"@tanstack/router-core": "1.171.13",
|
||||
"isbot": "^5.1.22"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0 || >=19.0.0",
|
||||
"react-dom": ">=18.0.0 || >=19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-store": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz",
|
||||
"integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/store": "0.9.3",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-core": {
|
||||
"version": "1.171.13",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.13.tgz",
|
||||
"integrity": "sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.162.0",
|
||||
"cookie-es": "^3.0.0",
|
||||
"seroval": "^1.5.4",
|
||||
"seroval-plugins": "^1.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/store": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz",
|
||||
"integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "9.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
|
||||
@@ -11489,6 +11395,13 @@
|
||||
"@types/unist": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/history": {
|
||||
"version": "4.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
|
||||
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
|
||||
@@ -11887,6 +11800,29 @@
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-router": {
|
||||
"version": "5.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
|
||||
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/history": "^4.7.11",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-router-dom": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
|
||||
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/history": "^4.7.11",
|
||||
"@types/react": "*",
|
||||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-syntax-highlighter": {
|
||||
"version": "15.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
||||
@@ -16491,12 +16427,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-es": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz",
|
||||
"integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
@@ -19479,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": {
|
||||
@@ -19489,7 +19419,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8",
|
||||
"storybook": "^10.4.1"
|
||||
"storybook": "^10.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-testing-library": {
|
||||
@@ -20991,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"
|
||||
@@ -24189,15 +24119,6 @@
|
||||
"url": "https://github.com/sponsors/gjtorikian/"
|
||||
}
|
||||
},
|
||||
"node_modules/isbot": {
|
||||
"version": "5.1.42",
|
||||
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.42.tgz",
|
||||
"integrity": "sha512-/SXsVh7KpPRISrD4ffrGSxnTLlUBzEQUfWIusaJPrpJ93FW1P0YEZri5vAUkFsA0m2HRUhQRQadk2wJ+EeKowQ==",
|
||||
"license": "Unlicense",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -36048,8 +35969,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
|
||||
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
@@ -36070,8 +35989,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
|
||||
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
@@ -36090,8 +36007,6 @@
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"loose-envify": "^1.2.0",
|
||||
@@ -36106,8 +36021,6 @@
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"loose-envify": "^1.2.0",
|
||||
@@ -36121,17 +36034,13 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-router/node_modules/path-to-regexp": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
|
||||
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
@@ -36140,9 +36049,7 @@
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-search-input": {
|
||||
"version": "0.11.3",
|
||||
@@ -37443,9 +37350,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
||||
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
@@ -37881,27 +37786,6 @@
|
||||
"integrity": "sha512-y9WzzDj3BsGgKLCh0ugiinufS//YqOfao/yVJjkXA4VLuyNCfHOLU/cbulGPxs3aeCqhvROw7qPL04JSZnCo0w==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/seroval": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz",
|
||||
"integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/seroval-plugins": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz",
|
||||
"integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"seroval": "^1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-index": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
|
||||
@@ -39152,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": {
|
||||
@@ -40186,16 +40070,13 @@
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinycolor2": {
|
||||
"version": "1.6.0",
|
||||
@@ -42198,9 +42079,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
@@ -45484,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",
|
||||
@@ -45532,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"
|
||||
|
||||
@@ -154,7 +154,6 @@
|
||||
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
||||
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
|
||||
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
||||
"@tanstack/react-router": "^1.170.15",
|
||||
"@types/d3-format": "^3.0.1",
|
||||
"@types/d3-selection": "^3.0.11",
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
@@ -179,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",
|
||||
@@ -219,6 +218,7 @@
|
||||
"react-redux": "^7.2.9",
|
||||
"react-resize-detector": "^9.1.1",
|
||||
"react-reverse-portal": "^2.3.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-search-input": "^0.11.3",
|
||||
"react-split": "^2.0.9",
|
||||
"react-table": "^7.8.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",
|
||||
@@ -289,6 +289,7 @@
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/redux-localstorage": "^1.0.8",
|
||||
@@ -324,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",
|
||||
@@ -354,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,
|
||||
|
||||
@@ -19,18 +19,18 @@
|
||||
|
||||
import { ThemeProvider } from '@apache-superset/core/theme';
|
||||
import querystring from 'query-string';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
||||
|
||||
export function ProviderWrapper(props: any) {
|
||||
const { children, theme } = props;
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<StandaloneRouter>
|
||||
<Router>
|
||||
<QueryParamProvider
|
||||
adapter={TanstackRouterAdapter}
|
||||
adapter={ReactRouter5Adapter}
|
||||
options={{
|
||||
searchStringToObject: querystring.parse,
|
||||
objectToSearchString: (object: Record<string, any>) =>
|
||||
@@ -39,7 +39,7 @@ export function ProviderWrapper(props: any) {
|
||||
>
|
||||
{children}
|
||||
</QueryParamProvider>
|
||||
</StandaloneRouter>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,14 +33,14 @@ import {
|
||||
} from '@apache-superset/core/theme';
|
||||
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
|
||||
import { ThemeController } from 'src/theme/ThemeController';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { DndContext } from '@dnd-kit/core';
|
||||
import reducerIndex from 'spec/helpers/reducerIndex';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
||||
import { configureStore, Store } from '@reduxjs/toolkit';
|
||||
import { api } from 'src/hooks/apiResources/queryApi';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -55,8 +55,6 @@ type Options = Omit<RenderOptions, 'queries'> & {
|
||||
initialState?: {};
|
||||
reducers?: {};
|
||||
store?: Store;
|
||||
/** Starting history entries for the test router (memory history). */
|
||||
initialEntries?: string[];
|
||||
};
|
||||
|
||||
const themeController = new ThemeController({ themeObject });
|
||||
@@ -86,7 +84,6 @@ export function createWrapper(options?: Options) {
|
||||
initialState,
|
||||
reducers,
|
||||
store,
|
||||
initialEntries,
|
||||
} = options || {};
|
||||
|
||||
return ({ children }: { children?: ReactNode }) => {
|
||||
@@ -119,18 +116,14 @@ export function createWrapper(options?: Options) {
|
||||
|
||||
if (useQueryParams) {
|
||||
result = (
|
||||
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
||||
{result}
|
||||
</QueryParamProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (useRouter || useQueryParams || initialEntries) {
|
||||
result = (
|
||||
<StandaloneRouter initialEntries={initialEntries}>
|
||||
{result}
|
||||
</StandaloneRouter>
|
||||
);
|
||||
if (useRouter) {
|
||||
result = <BrowserRouter>{result}</BrowserRouter>;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Navigate } from '@tanstack/react-router';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { css, styled } from '@apache-superset/core/theme';
|
||||
@@ -208,7 +208,13 @@ class App extends PureComponent<AppProps, AppState> {
|
||||
render() {
|
||||
const { queries, queriesLastUpdate } = this.props;
|
||||
if (this.state.hash && this.state.hash === '#search') {
|
||||
return <Navigate to="/sqllab/history/" replace />;
|
||||
return (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/sqllab/history/',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SqlLabStyles data-test="SqlLabApp" className="App SqlLab">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render, waitFor } from 'spec/helpers/testing-library';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { initialState } from 'src/SqlLab/fixtures';
|
||||
@@ -32,11 +32,11 @@ const setup = (
|
||||
overridesInitialState?: RootState,
|
||||
) =>
|
||||
render(
|
||||
<StandaloneRouter initialEntries={[url]}>
|
||||
<MemoryRouter initialEntries={[url]}>
|
||||
<LocationProvider>
|
||||
<PopEditorTab />
|
||||
</LocationProvider>
|
||||
</StandaloneRouter>,
|
||||
</MemoryRouter>,
|
||||
{
|
||||
useRedux: true,
|
||||
initialState: overridesInitialState || initialState,
|
||||
|
||||
@@ -30,8 +30,7 @@ import {
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { useAppDispatch } from 'src/SqlLab/hooks/useAppDispatch';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { pick } from 'lodash';
|
||||
import {
|
||||
Button,
|
||||
@@ -233,7 +232,7 @@ const ResultSet = ({
|
||||
canExportDataSqlLab: canExportData,
|
||||
canCopyClipboardSqlLab: canCopyClipboard,
|
||||
} = usePermissions();
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const logAction = useLogAction({ queryId, sqlEditorId: query.sqlEditorId });
|
||||
const { showConfirm, ConfirmModal } = useConfirmModal();
|
||||
@@ -315,7 +314,7 @@ const ResultSet = ({
|
||||
if (openInNewWindow) {
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
} else {
|
||||
pushAppHref(router, url);
|
||||
history.push(url);
|
||||
}
|
||||
} else {
|
||||
addDangerToast(t('Unable to create chart without a query id.'));
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
import { css, useTheme } from '@apache-superset/core/theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
@@ -78,7 +78,7 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { addDangerToast } = useToasts();
|
||||
const theme = useTheme();
|
||||
const [formDataKey, setFormDataKey] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const dashboardPageId = useContext(DashboardPageIdContext);
|
||||
const onEditChartClick = useCallback(() => {
|
||||
dispatch(
|
||||
@@ -97,7 +97,9 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
|
||||
if (isEmbedded()) return;
|
||||
postFormData(Number(datasource_id), datasource_type, formData, 0)
|
||||
.then(key => {
|
||||
setFormDataKey(key);
|
||||
setUrl(
|
||||
`/explore/?form_data_key=${key}&dashboard_page_id=${dashboardPageId}`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
addDangerToast(t('Failed to generate chart edit URL'));
|
||||
@@ -109,7 +111,7 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
|
||||
datasource_type,
|
||||
formData,
|
||||
]);
|
||||
const isEditDisabled = !formDataKey || !canExplore;
|
||||
const isEditDisabled = !url || !canExplore;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -131,11 +133,7 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
|
||||
text-decoration: none;
|
||||
}
|
||||
`}
|
||||
to="/explore/"
|
||||
search={{
|
||||
form_data_key: formDataKey,
|
||||
dashboard_page_id: dashboardPageId,
|
||||
}}
|
||||
to={url}
|
||||
>
|
||||
{t('Edit chart')}
|
||||
</Link>
|
||||
|
||||
@@ -25,12 +25,10 @@ import DrillDetailModal from './DrillDetailModal';
|
||||
|
||||
jest.mock('./DrillDetailPane', () => () => null);
|
||||
const mockHistoryPush = jest.fn();
|
||||
jest.mock('@tanstack/react-router', () => ({
|
||||
...jest.requireActual('@tanstack/react-router'),
|
||||
useRouter: () => ({
|
||||
history: {
|
||||
push: mockHistoryPush,
|
||||
},
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: () => ({
|
||||
push: mockHistoryPush,
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
*/
|
||||
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
BinaryQueryObjectFilterClause,
|
||||
@@ -99,7 +98,7 @@ export default function DrillDetailModal({
|
||||
dataset,
|
||||
}: DrillDetailModalProps) {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const dashboardPageId = useContext(DashboardPageIdContext);
|
||||
const { slice_name: chartName } = useSelector(
|
||||
(state: { sliceEntities: { slices: Record<number, Slice> } }) =>
|
||||
@@ -115,8 +114,8 @@ export default function DrillDetailModal({
|
||||
);
|
||||
|
||||
const exploreChart = useCallback(() => {
|
||||
pushAppHref(router, exploreUrl);
|
||||
}, [exploreUrl, router]);
|
||||
history.push(exploreUrl);
|
||||
}, [exploreUrl, history]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -17,49 +17,32 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import { AnchorHTMLAttributes } from 'react';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { parseSearch } from 'src/router/searchParams';
|
||||
import { PropsWithoutRef, RefAttributes } from 'react';
|
||||
import { Link, LinkProps } from 'react-router-dom';
|
||||
import { isUrlExternal, parseUrl } from 'src/utils/urlUtils';
|
||||
|
||||
export type GenericLinkProps = Omit<
|
||||
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
'href'
|
||||
> & {
|
||||
to: string;
|
||||
replace?: boolean;
|
||||
};
|
||||
|
||||
export const GenericLink = ({
|
||||
to: rawTo,
|
||||
export const GenericLink = <S,>({
|
||||
to,
|
||||
component,
|
||||
replace,
|
||||
innerRef,
|
||||
children,
|
||||
...rest
|
||||
}: GenericLinkProps) => {
|
||||
// Callers may pass undefined at runtime (e.g. backend rows without a URL).
|
||||
const to = typeof rawTo === 'string' ? rawTo : '';
|
||||
if (to && isUrlExternal(to)) {
|
||||
}: PropsWithoutRef<LinkProps<S>> & RefAttributes<HTMLAnchorElement>) => {
|
||||
if (typeof to === 'string' && isUrlExternal(to)) {
|
||||
return (
|
||||
<a data-test="external-link" href={sanitizeUrl(parseUrl(to))} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const hashIndex = to.indexOf('#');
|
||||
const hash = hashIndex === -1 ? undefined : to.slice(hashIndex + 1);
|
||||
const withoutHash = hashIndex === -1 ? to : to.slice(0, hashIndex);
|
||||
const searchIndex = withoutHash.indexOf('?');
|
||||
const pathname =
|
||||
searchIndex === -1 ? withoutHash : withoutHash.slice(0, searchIndex);
|
||||
const searchStr =
|
||||
searchIndex === -1 ? '' : withoutHash.slice(searchIndex + 1);
|
||||
return (
|
||||
<Link
|
||||
data-test="internal-link"
|
||||
to={pathname}
|
||||
search={searchStr ? parseSearch(searchStr) : undefined}
|
||||
hash={hash}
|
||||
to={to}
|
||||
component={component}
|
||||
replace={replace}
|
||||
innerRef={innerRef}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTruncation } from '@superset-ui/core';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import CrossLinksTooltip from './CrossLinksTooltip';
|
||||
|
||||
export type CrossLinkProps = {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Tooltip } from '@superset-ui/core/components';
|
||||
|
||||
export type CrossLinksTooltipProps = {
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
import { render, screen, within, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { ReactNode } from 'react';
|
||||
@@ -206,11 +206,11 @@ test('redirects to first page when page index is invalid', async () => {
|
||||
const factory = (overrides?: Partial<ListViewProps>) => {
|
||||
const props = { ...mockedPropsComprehensive, ...overrides };
|
||||
return render(
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||
<MemoryRouter>
|
||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
||||
<ListView {...props} />
|
||||
</QueryParamProvider>
|
||||
</StandaloneRouter>,
|
||||
</MemoryRouter>,
|
||||
{ store: mockStore() },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { TagType } from 'src/types/TagType';
|
||||
import { Tag as AntdTag } from '@superset-ui/core/components/Tag';
|
||||
import { Tooltip } from '@superset-ui/core/components/Tooltip';
|
||||
@@ -82,8 +82,7 @@ const SupersetTag = ({
|
||||
{' '}
|
||||
{id ? (
|
||||
<Link
|
||||
to="/superset/all_entities/"
|
||||
search={{ id: String(id) }}
|
||||
to={`/superset/all_entities/?id=${id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import { DataMaskStateWithId, JsonObject } from '@superset-ui/core';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import type { RouterHistory } from '@tanstack/react-router';
|
||||
import type { History } from 'history';
|
||||
import { chart } from 'src/components/Chart/chartReducer';
|
||||
import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities';
|
||||
import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters';
|
||||
@@ -92,7 +92,7 @@ interface HydrateDashboardData extends Dashboard {
|
||||
}
|
||||
|
||||
interface HydrateDashboardParams {
|
||||
history: RouterHistory;
|
||||
history: History;
|
||||
dashboard: HydrateDashboardData;
|
||||
charts: HydrateChartData[];
|
||||
dataMask: DataMaskStateWithId;
|
||||
@@ -278,10 +278,9 @@ export const hydrateDashboard =
|
||||
// Removes the focused_chart parameter from the URL
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.delete(URL_PARAMS.dashboardFocusedChart.name);
|
||||
const paramString = params.toString();
|
||||
history.replace(
|
||||
`${history.location.pathname}${paramString ? `?${paramString}` : ''}`,
|
||||
);
|
||||
history.replace({
|
||||
search: params.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
// find direct link component and path from root
|
||||
|
||||
@@ -32,16 +32,14 @@ import { UPDATE_COMPONENTS } from '../../actions/dashboardLayout';
|
||||
import { AutoRefreshStatus } from '../../types/autoRefresh';
|
||||
|
||||
const mockHistoryReplace = jest.fn();
|
||||
jest.mock('@tanstack/react-router', () => ({
|
||||
...jest.requireActual('@tanstack/react-router'),
|
||||
useRouter: () => ({
|
||||
history: {
|
||||
replace: mockHistoryReplace,
|
||||
},
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: () => ({
|
||||
replace: mockHistoryReplace,
|
||||
}),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/dashboard',
|
||||
searchStr: 'standalone=1',
|
||||
search: '?standalone=1',
|
||||
hash: '',
|
||||
state: undefined,
|
||||
})),
|
||||
@@ -239,10 +237,10 @@ beforeAll(() => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const { useLocation } = jest.requireMock('@tanstack/react-router');
|
||||
const { useLocation } = jest.requireMock('react-router-dom');
|
||||
useLocation.mockReturnValue({
|
||||
pathname: '/dashboard',
|
||||
searchStr: 'standalone=1',
|
||||
search: '?standalone=1',
|
||||
hash: '',
|
||||
state: undefined,
|
||||
});
|
||||
@@ -1053,11 +1051,11 @@ test('should sync theme ref when navigating between dashboards', async () => {
|
||||
});
|
||||
|
||||
test('should not duplicate subdirectory prefix when toggling fullscreen', async () => {
|
||||
const { useLocation } = jest.requireMock('@tanstack/react-router');
|
||||
const { useLocation } = jest.requireMock('react-router-dom');
|
||||
// Simulate React Router with basename=/pcs: useLocation returns path relative to basename
|
||||
useLocation.mockReturnValue({
|
||||
pathname: '/dashboard',
|
||||
searchStr: 'standalone=1',
|
||||
search: '?standalone=1',
|
||||
hash: '',
|
||||
state: undefined,
|
||||
});
|
||||
@@ -1080,10 +1078,10 @@ test('should not duplicate subdirectory prefix when toggling fullscreen', async
|
||||
});
|
||||
|
||||
test('should not duplicate subdirectory prefix when entering fullscreen', async () => {
|
||||
const { useLocation } = jest.requireMock('@tanstack/react-router');
|
||||
const { useLocation } = jest.requireMock('react-router-dom');
|
||||
useLocation.mockReturnValue({
|
||||
pathname: '/dashboard',
|
||||
searchStr: '',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: undefined,
|
||||
});
|
||||
@@ -1102,11 +1100,11 @@ test('should not duplicate subdirectory prefix when entering fullscreen', async
|
||||
});
|
||||
|
||||
test('share URL should use browser-absolute pathname to preserve subdirectory prefix', () => {
|
||||
const { useLocation } = jest.requireMock('@tanstack/react-router');
|
||||
const { useLocation } = jest.requireMock('react-router-dom');
|
||||
// Router returns path without the subdirectory prefix
|
||||
useLocation.mockReturnValue({
|
||||
pathname: '/dashboard',
|
||||
searchStr: '',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: undefined,
|
||||
});
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
import type { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation, useRouter } from '@tanstack/react-router';
|
||||
import { replaceAppHref } from 'src/router/navigation';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Menu, MenuItem } from '@superset-ui/core/components/Menu';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { isEmpty } from 'lodash';
|
||||
@@ -75,7 +74,7 @@ export const useHeaderActionsMenu = ({
|
||||
] => {
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||
const { canExportImage } = usePermissions();
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const directPathToChild = useSelector(
|
||||
(state: RootState) => state.dashboardState.directPathToChild,
|
||||
@@ -103,7 +102,7 @@ export const useHeaderActionsMenu = ({
|
||||
case MenuKeys.ToggleFullscreen: {
|
||||
const isCurrentlyStandalone =
|
||||
Number(getUrlParam(URL_PARAMS.standalone)) === 1;
|
||||
// Use location.pathname from the router (relative to basepath) rather than
|
||||
// Use location.pathname from React Router (relative to basename) rather than
|
||||
// window.location.pathname to avoid duplicating the subdirectory prefix when
|
||||
// history.replace prepends it again.
|
||||
const url = getDashboardUrl({
|
||||
@@ -112,7 +111,7 @@ export const useHeaderActionsMenu = ({
|
||||
hash: window.location.hash,
|
||||
standalone: isCurrentlyStandalone ? null : 1,
|
||||
});
|
||||
replaceAppHref(router, url);
|
||||
history.replace(url);
|
||||
break;
|
||||
}
|
||||
case MenuKeys.ManageEmbedded:
|
||||
@@ -129,7 +128,7 @@ export const useHeaderActionsMenu = ({
|
||||
showPropertiesModal,
|
||||
showRefreshModal,
|
||||
manageEmbedded,
|
||||
router,
|
||||
history,
|
||||
location,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -16,15 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { createMemoryHistory } from '@tanstack/react-router';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { getExtensionsRegistry, VizType } from '@superset-ui/core';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
|
||||
import { useUiConfig } from 'src/components/UiConfigContext';
|
||||
import SliceHeader from '.';
|
||||
@@ -288,12 +283,12 @@ test('Should render click to edit prompt and run onExploreChart on click', async
|
||||
initialEntries: ['/superset/dashboard/1/'],
|
||||
});
|
||||
render(
|
||||
<StandaloneRouter history={history}>
|
||||
<Router history={history}>
|
||||
<SliceHeader {...props} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ useRedux: true, initialState },
|
||||
);
|
||||
userEvent.hover(await screen.findByText('Vaccine Candidates per Phase'));
|
||||
userEvent.hover(screen.getByText('Vaccine Candidates per Phase'));
|
||||
expect(
|
||||
await screen.findByText('Click to edit Vaccine Candidates per Phase.'),
|
||||
).toBeInTheDocument();
|
||||
@@ -302,8 +297,7 @@ test('Should render click to edit prompt and run onExploreChart on click', async
|
||||
).toBeInTheDocument();
|
||||
|
||||
userEvent.click(screen.getByText('Vaccine Candidates per Phase'));
|
||||
// TanStack router commits navigation asynchronously.
|
||||
await waitFor(() => expect(history.location.pathname).toMatch('/explore'));
|
||||
expect(history.location.pathname).toMatch('/explore');
|
||||
});
|
||||
|
||||
test('Display cmd button in tooltip if running on MacOS', async () => {
|
||||
@@ -323,18 +317,18 @@ test('Display cmd button in tooltip if running on MacOS', async () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should not render click to edit prompt and run onExploreChart on click if supersetCanExplore=false', async () => {
|
||||
test('Should not render click to edit prompt and run onExploreChart on click if supersetCanExplore=false', () => {
|
||||
const props = createProps({ supersetCanExplore: false });
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/superset/dashboard/1/'],
|
||||
});
|
||||
render(
|
||||
<StandaloneRouter history={history}>
|
||||
<Router history={history}>
|
||||
<SliceHeader {...props} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ useRedux: true, initialState },
|
||||
);
|
||||
userEvent.hover(await screen.findByText('Vaccine Candidates per Phase'));
|
||||
userEvent.hover(screen.getByText('Vaccine Candidates per Phase'));
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'Click to edit Vaccine Candidates per Phase in a new tab',
|
||||
@@ -345,18 +339,18 @@ test('Should not render click to edit prompt and run onExploreChart on click if
|
||||
expect(history.location.pathname).toMatch('/superset/dashboard');
|
||||
});
|
||||
|
||||
test('Should not render click to edit prompt and run onExploreChart on click if in edit mode', async () => {
|
||||
test('Should not render click to edit prompt and run onExploreChart on click if in edit mode', () => {
|
||||
const props = createProps({ editMode: true });
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/superset/dashboard/1/'],
|
||||
});
|
||||
render(
|
||||
<StandaloneRouter history={history}>
|
||||
<Router history={history}>
|
||||
<SliceHeader {...props} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ useRedux: true, initialState },
|
||||
);
|
||||
userEvent.hover(await screen.findByText('Vaccine Candidates per Phase'));
|
||||
userEvent.hover(screen.getByText('Vaccine Candidates per Phase'));
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'Click to edit Vaccine Candidates per Phase in a new tab',
|
||||
|
||||
@@ -45,7 +45,7 @@ import { RootState } from 'src/dashboard/types';
|
||||
import { getSliceHeaderTooltip } from 'src/dashboard/util/getSliceHeaderTooltip';
|
||||
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
|
||||
import RowCountLabel from 'src/components/RowCountLabel';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const extensionsRegistry = getExtensionsRegistry();
|
||||
|
||||
@@ -245,11 +245,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
|
||||
|
||||
const renderExploreLink = (title: string) => (
|
||||
<Link
|
||||
to="/explore/"
|
||||
search={{
|
||||
dashboard_page_id: dashboardPageId,
|
||||
slice_id: String(slice.slice_id),
|
||||
}}
|
||||
to={exploreUrl}
|
||||
css={(theme: SupersetTheme) => css`
|
||||
color: ${theme.colorText};
|
||||
text-decoration: none;
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactChild, RefObject, useCallback } from 'react';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { css, useTheme } from '@apache-superset/core/theme';
|
||||
import { Button, ModalTrigger } from '@superset-ui/core/components';
|
||||
@@ -38,9 +37,8 @@ export const ViewResultsModalTrigger = ({
|
||||
modalBody: ReactChild;
|
||||
modalRef?: RefObject<any>;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
// exploreUrl carries a query string; raw history push preserves it.
|
||||
const exploreChart = () => pushAppHref(router, exploreUrl);
|
||||
const history = useHistory();
|
||||
const exploreChart = () => history.push(exploreUrl);
|
||||
const theme = useTheme();
|
||||
const handleCloseModal = useCallback(() => {
|
||||
modalRef?.current?.close();
|
||||
|
||||
@@ -26,8 +26,7 @@ import {
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { RouteComponentProps, useHistory } from 'react-router-dom';
|
||||
import { extendedDayjs } from '@superset-ui/core/utils/dates';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
@@ -145,7 +144,8 @@ export interface SliceHeaderControlsProps {
|
||||
|
||||
crossFiltersEnabled?: boolean;
|
||||
}
|
||||
type SliceHeaderControlsPropsWithRouter = SliceHeaderControlsProps;
|
||||
type SliceHeaderControlsPropsWithRouter = SliceHeaderControlsProps &
|
||||
RouteComponentProps;
|
||||
|
||||
const dropdownIconsStyles = css`
|
||||
&&.anticon > .anticon:first-of-type {
|
||||
@@ -169,7 +169,7 @@ const SliceHeaderControls = (
|
||||
const [openScopingModal, scopingModal] = useCrossFiltersScopingModal(
|
||||
props.slice.slice_id,
|
||||
);
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
|
||||
const queryMenuRef: RefObject<any> = useRef(null);
|
||||
const resultsMenuRef: RefObject<any> = useRef(null);
|
||||
@@ -265,8 +265,7 @@ const SliceHeaderControls = (
|
||||
domEvent.preventDefault();
|
||||
window.open(props.exploreUrl, '_blank');
|
||||
} else {
|
||||
// exploreUrl carries a query string; raw history push preserves it.
|
||||
pushAppHref(router, props.exploreUrl);
|
||||
history.push(props.exploreUrl);
|
||||
}
|
||||
break;
|
||||
case MenuKeys.ExportCsv:
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -30,7 +30,7 @@ import { FilterBarOrientation, RootState } from 'src/dashboard/types';
|
||||
import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
|
||||
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation, useRouter } from '@tanstack/react-router';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { removeDataMask, updateDataMask } from 'src/dataMask/actions';
|
||||
import {
|
||||
getRisonFilterParam,
|
||||
@@ -121,8 +121,8 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
|
||||
state => state.dataMask,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const router = useRouter();
|
||||
const searchStr = useLocation({ select: location => location.searchStr });
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const chartIds = useChartIds();
|
||||
const chartLayoutItems = useChartLayoutItems();
|
||||
const verboseMaps = useChartsVerboseMaps();
|
||||
@@ -146,7 +146,7 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
|
||||
// programmatic history.replace).
|
||||
useEffect(() => {
|
||||
setActiveUrlFilters(getUrlFilterIndicators());
|
||||
}, [searchStr]);
|
||||
}, [location.search]);
|
||||
|
||||
const handleRemoveUrlFilter = useCallback(
|
||||
(filterToRemove: UrlFilterIndicator) => {
|
||||
@@ -158,7 +158,7 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
|
||||
const remaining = currentFilters.filter(
|
||||
f => getUrlFilterIdentity(f) !== removeId,
|
||||
);
|
||||
updateUrlWithUnmatchedFilters(remaining, router.history);
|
||||
updateUrlWithUnmatchedFilters(remaining, history);
|
||||
setActiveUrlFilters(prev =>
|
||||
prev.filter(f => getUrlFilterIdentity(f.filter) !== removeId),
|
||||
);
|
||||
@@ -175,7 +175,7 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch, router],
|
||||
[dispatch, history],
|
||||
);
|
||||
|
||||
const urlFiltersComponent = useMemo(() => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -24,15 +24,9 @@
|
||||
* - the chip list must react to URL changes (back/forward navigation or
|
||||
* a programmatic history.replace), not snapshot the URL at mount.
|
||||
*/
|
||||
import { createMemoryHistory } from '@tanstack/react-router';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import {
|
||||
act,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { act, render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import { REMOVE_DATA_MASK, UPDATE_DATA_MASK } from 'src/dataMask/actions';
|
||||
import { RISON_UNMATCHED_DATAMASK_ID } from 'src/dashboard/util/risonFilters';
|
||||
import UrlFiltersVertical from './Vertical';
|
||||
@@ -45,7 +39,7 @@ jest.mock('react-redux', () => ({
|
||||
|
||||
const seedUrl = (search: string) => {
|
||||
// jsdom doesn't navigate, so set both window.location (read by
|
||||
// getRisonFilterParam) and the router's in-memory history.
|
||||
// getRisonFilterParam) and react-router's in-memory history.
|
||||
window.history.replaceState({}, '', `/superset/dashboard/1/${search}`);
|
||||
};
|
||||
|
||||
@@ -55,9 +49,9 @@ const renderAt = (search: string) => {
|
||||
initialEntries: [`/superset/dashboard/1/${search}`],
|
||||
});
|
||||
const utils = render(
|
||||
<StandaloneRouter history={history}>
|
||||
<Router history={history}>
|
||||
<UrlFiltersVertical />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ useRedux: true },
|
||||
);
|
||||
return { ...utils, history };
|
||||
@@ -126,7 +120,7 @@ test('removing the last chip dispatches removeDataMask, not an empty update', as
|
||||
expect(updateCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('chip list re-renders when the URL changes (popstate/programmatic nav)', async () => {
|
||||
test('chip list re-renders when the URL changes (popstate/programmatic nav)', () => {
|
||||
const { history } = renderAt('?f=(region:EMEA)');
|
||||
|
||||
expect(screen.getByText('region')).toBeInTheDocument();
|
||||
@@ -139,8 +133,7 @@ test('chip list re-renders when the URL changes (popstate/programmatic nav)', as
|
||||
history.replace('/superset/dashboard/1/?f=(priority:high)');
|
||||
});
|
||||
|
||||
// The router commits location updates asynchronously.
|
||||
await waitFor(() => expect(screen.getByText('priority')).toBeInTheDocument());
|
||||
expect(screen.getByText('priority')).toBeInTheDocument();
|
||||
expect(screen.getByText('high')).toBeInTheDocument();
|
||||
expect(screen.queryByText('region')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation, useRouter } from '@tanstack/react-router';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { QueryObjectFilterClause } from '@superset-ui/core';
|
||||
import { removeDataMask, updateDataMask } from 'src/dataMask/actions';
|
||||
import {
|
||||
@@ -38,8 +38,8 @@ import UrlFiltersVerticalCollapse from './VerticalCollapse';
|
||||
|
||||
const UrlFiltersVertical = () => {
|
||||
const dispatch = useDispatch();
|
||||
const router = useRouter();
|
||||
const searchStr = useLocation({ select: location => location.searchStr });
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [urlFilters, setUrlFilters] = useState<UrlFilterIndicator[]>(() =>
|
||||
getUrlFilterIndicators(),
|
||||
);
|
||||
@@ -48,7 +48,7 @@ const UrlFiltersVertical = () => {
|
||||
// programmatic history.replace).
|
||||
useEffect(() => {
|
||||
setUrlFilters(getUrlFilterIndicators());
|
||||
}, [searchStr]);
|
||||
}, [location.search]);
|
||||
|
||||
const handleRemoveFilter = useCallback(
|
||||
(filterToRemove: UrlFilterIndicator) => {
|
||||
@@ -61,7 +61,7 @@ const UrlFiltersVertical = () => {
|
||||
f => getUrlFilterIdentity(f) !== removeId,
|
||||
);
|
||||
|
||||
updateUrlWithUnmatchedFilters(remaining, router.history);
|
||||
updateUrlWithUnmatchedFilters(remaining, history);
|
||||
setUrlFilters(prev =>
|
||||
prev.filter(f => getUrlFilterIdentity(f.filter) !== removeId),
|
||||
);
|
||||
@@ -78,7 +78,7 @@ const UrlFiltersVertical = () => {
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch, router],
|
||||
[dispatch, history],
|
||||
);
|
||||
|
||||
if (!urlFilters.length) {
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Constants } from '@superset-ui/core/components';
|
||||
import { useRouter, type RouterHistory } from '@tanstack/react-router';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { updateDataMask, removeDataMask } from 'src/dataMask/actions';
|
||||
import {
|
||||
saveChartCustomization,
|
||||
@@ -55,6 +55,7 @@ import { useImmer } from 'use-immer';
|
||||
import { isEmpty, isEqual, debounce } from 'lodash';
|
||||
import { getInitialDataMask } from 'src/dataMask/reducer';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { applicationRoot } from 'src/utils/getBootstrapData';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { useTabId } from 'src/hooks/useTabId';
|
||||
import { logEvent } from 'src/logger/actions';
|
||||
@@ -95,7 +96,7 @@ const EMPTY_DATA_MASK_RECORD: Record<string, DataMask> = {};
|
||||
|
||||
const publishDataMask = debounce(
|
||||
async (
|
||||
history: RouterHistory,
|
||||
history,
|
||||
dashboardId,
|
||||
updateKey,
|
||||
dataMaskSelected: DataMaskStateWithId,
|
||||
@@ -144,10 +145,15 @@ const publishDataMask = debounce(
|
||||
// replace params only when current page is /superset/dashboard
|
||||
// this prevents a race condition between updating filters and navigating to Explore
|
||||
if (window.location.pathname.includes('/superset/dashboard')) {
|
||||
// The router's history is the raw browser history (no basepath
|
||||
// handling), so the full window pathname — application root
|
||||
// included — is replaced verbatim.
|
||||
const replacementPathname = window.location.pathname;
|
||||
// The history API is part of React router and understands that a basename may exist.
|
||||
// Internally it treats all paths as if they are relative to the root and appends
|
||||
// it when necessary. We strip any prefix so that history.replace adds it back and doesn't
|
||||
// double it up.
|
||||
const appRoot = applicationRoot();
|
||||
let replacementPathname = window.location.pathname;
|
||||
if (appRoot !== '/' && replacementPathname.startsWith(appRoot)) {
|
||||
replacementPathname = replacementPathname.substring(appRoot.length);
|
||||
}
|
||||
// Manually reconstruct the search string to preserve Rison filter encoding
|
||||
let searchString = newParams.toString();
|
||||
if (rawRisonFilterValue) {
|
||||
@@ -155,9 +161,10 @@ const publishDataMask = debounce(
|
||||
searchString = `${searchString}${separator}f=${rawRisonFilterValue}`;
|
||||
}
|
||||
|
||||
history.replace(
|
||||
`${replacementPathname}${searchString ? `?${searchString}` : ''}`,
|
||||
);
|
||||
history.replace({
|
||||
pathname: replacementPathname,
|
||||
search: searchString,
|
||||
});
|
||||
}
|
||||
},
|
||||
Constants.SLOW_DEBOUNCE,
|
||||
@@ -168,7 +175,7 @@ const FilterBar: FC<FiltersBarProps> = ({
|
||||
verticalConfig,
|
||||
hidden = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const dataMaskApplied: DataMaskStateWithId = useAllAppliedDataMask();
|
||||
|
||||
const [dataMaskSelected, setDataMaskSelected] =
|
||||
@@ -399,16 +406,10 @@ const FilterBar: FC<FiltersBarProps> = ({
|
||||
useEffect(() => {
|
||||
// embedded users can't persist filter combinations
|
||||
if (user?.userId) {
|
||||
publishDataMask(
|
||||
router.history,
|
||||
dashboardId,
|
||||
updateKey,
|
||||
dataMaskApplied,
|
||||
tabId,
|
||||
);
|
||||
publishDataMask(history, dashboardId, updateKey, dataMaskApplied, tabId);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardId, dataMaskAppliedText, router, updateKey, tabId]);
|
||||
}, [dashboardId, dataMaskAppliedText, history, updateKey, tabId]);
|
||||
|
||||
const pendingChartCustomizations = useSelector<
|
||||
RootState,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { createContext, lazy, FC, useEffect, useMemo, useRef } from 'react';
|
||||
import { Global } from '@emotion/react';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -128,7 +128,7 @@ const selectActiveFilters = createSelector(
|
||||
export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const dashboardPageId = useMemo(() => nanoid(), []);
|
||||
const hasDashboardInfoInitiated = useSelector<RootState, boolean>(
|
||||
({ dashboardInfo }) =>
|
||||
@@ -267,14 +267,14 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||
|
||||
// Rewrite the URL to drop matched filters in a single step, keeping
|
||||
// only unmatched ones (and prettifying their encoding). Going
|
||||
// through the router's history keeps its location.search in
|
||||
// through react-router's history keeps `history.location.search` in
|
||||
// sync so `publishDataMask` doesn't re-emit the original `f=`.
|
||||
const matchedCount =
|
||||
risonFilters.length - injectionResult.unmatchedFilters.length;
|
||||
if (matchedCount > 0) {
|
||||
updateUrlWithUnmatchedFilters(
|
||||
injectionResult.unmatchedFilters,
|
||||
router.history,
|
||||
history,
|
||||
);
|
||||
}
|
||||
if (injectionResult.unmatchedFilters.length > 0) {
|
||||
@@ -289,7 +289,7 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||
}
|
||||
dispatch(
|
||||
hydrateDashboard({
|
||||
history: router.history,
|
||||
history,
|
||||
dashboard: dashboard!,
|
||||
charts: charts!,
|
||||
activeTabs: activeTabs ?? null,
|
||||
|
||||
@@ -353,10 +353,10 @@ test('updateUrlWithUnmatchedFilters goes through history when supplied', () => {
|
||||
);
|
||||
|
||||
expect(replace).toHaveBeenCalledTimes(1);
|
||||
const href = replace.mock.calls[0][0];
|
||||
expect(href).toMatch(/^\/superset\/dashboard\/1\/\?/);
|
||||
expect(href).toContain('f=');
|
||||
expect(href).toContain('region');
|
||||
const call = replace.mock.calls[0][0];
|
||||
expect(call.pathname).toBe('/superset/dashboard/1/');
|
||||
expect(call.search).toContain('f=');
|
||||
expect(call.search).toContain('region');
|
||||
|
||||
// Restore.
|
||||
window.history.replaceState({}, '', originalLocation);
|
||||
@@ -370,7 +370,7 @@ test('updateUrlWithUnmatchedFilters drops f= when no unmatched remain', () => {
|
||||
updateUrlWithUnmatchedFilters([], { replace });
|
||||
|
||||
expect(replace).toHaveBeenCalledTimes(1);
|
||||
expect(replace.mock.calls[0][0]).toBe('/superset/dashboard/1/');
|
||||
expect(replace.mock.calls[0][0].search).toBe('');
|
||||
|
||||
window.history.replaceState({}, '', originalLocation);
|
||||
});
|
||||
@@ -382,20 +382,16 @@ test('updateUrlWithUnmatchedFilters cleanup is observable by history readers', (
|
||||
// history.location.search stale, causing publishDataMask to re-append
|
||||
// the original f= on the next interaction.
|
||||
//
|
||||
// Stand in for the router's history with a fake whose `.location`
|
||||
// Stand in for react-router's history with a fake whose `.location`
|
||||
// updates synchronously when .replace is called — same contract as
|
||||
// the router history's replace.
|
||||
// react-router-dom's history.replace.
|
||||
const fakeHistory = {
|
||||
location: {
|
||||
pathname: '/superset/dashboard/1/',
|
||||
search: '?f=(country:USA)',
|
||||
},
|
||||
replace(href: string) {
|
||||
const [pathname, search = ''] = href.split('?');
|
||||
this.location = {
|
||||
pathname,
|
||||
search: search ? `?${search}` : '',
|
||||
};
|
||||
replace(next: { pathname: string; search: string }) {
|
||||
this.location = next;
|
||||
},
|
||||
};
|
||||
const originalLocation = window.location.href;
|
||||
|
||||
@@ -318,15 +318,15 @@ export function risonFiltersToString(filters: RisonFilter[]): string {
|
||||
}
|
||||
|
||||
interface ReplaceHistory {
|
||||
replace(href: string): void;
|
||||
replace(location: { pathname: string; search: string }): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the URL to remove successfully matched filters, keeping only unmatched ones.
|
||||
* When a router history is supplied (e.g. `useRouter().history`), the update
|
||||
* goes through it so that components reading the router location (e.g.
|
||||
* `publishDataMask` in the filter bar) see the new search string. Otherwise
|
||||
* falls back to a raw `window.history.replaceState`.
|
||||
* When a react-router history is supplied, the update goes through it so that
|
||||
* components reading from `history.location` (e.g. `publishDataMask` in the
|
||||
* filter bar) see the new search string. Otherwise falls back to a raw
|
||||
* `window.history.replaceState`.
|
||||
*/
|
||||
export function updateUrlWithUnmatchedFilters(
|
||||
unmatchedFilters: RisonFilter[],
|
||||
@@ -358,7 +358,10 @@ export function updateUrlWithUnmatchedFilters(
|
||||
currentUrl.toString(),
|
||||
);
|
||||
if (history) {
|
||||
history.replace(`${currentUrl.pathname}${currentUrl.search}`);
|
||||
history.replace({
|
||||
pathname: currentUrl.pathname,
|
||||
search: currentUrl.search,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to update URL with unmatched filters:', error);
|
||||
|
||||
@@ -20,13 +20,7 @@ import 'src/public-path';
|
||||
|
||||
import { lazy, Suspense, useEffect } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import {
|
||||
createRootRoute,
|
||||
createRoute,
|
||||
createRouter,
|
||||
RouterProvider,
|
||||
} from '@tanstack/react-router';
|
||||
import { parseSearch, stringifySearch } from 'src/router/searchParams';
|
||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||
import { Global } from '@emotion/react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { makeApi } from '@superset-ui/core';
|
||||
@@ -122,29 +116,13 @@ const EmbeddedRoute = () => (
|
||||
</EmbeddedContextProviders>
|
||||
);
|
||||
|
||||
const embeddedRootRoute = createRootRoute();
|
||||
const embeddedRouter = createRouter({
|
||||
routeTree: embeddedRootRoute.addChildren([
|
||||
// todo (embedded) remove this route after uuids are deployed
|
||||
createRoute({
|
||||
getParentRoute: () => embeddedRootRoute,
|
||||
path: '/dashboard/$idOrSlug/embedded',
|
||||
component: EmbeddedRoute,
|
||||
}),
|
||||
createRoute({
|
||||
getParentRoute: () => embeddedRootRoute,
|
||||
path: '/embedded/$uuid',
|
||||
component: EmbeddedRoute,
|
||||
}),
|
||||
]),
|
||||
basepath: applicationRoot() || undefined,
|
||||
parseSearch,
|
||||
stringifySearch,
|
||||
trailingSlash: 'preserve',
|
||||
defaultPreload: false,
|
||||
});
|
||||
|
||||
const EmbeddedApp = () => <RouterProvider router={embeddedRouter} />;
|
||||
const EmbeddedApp = () => (
|
||||
<Router basename={applicationRoot()}>
|
||||
{/* todo (embedded) remove this line after uuids are deployed */}
|
||||
<Route path="/dashboard/:idOrSlug/embedded/" component={EmbeddedRoute} />
|
||||
<Route path="/embedded/:uuid/" component={EmbeddedRoute} />
|
||||
</Router>
|
||||
);
|
||||
|
||||
const appMountPoint = document.getElementById('app')!;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter, type RouterHistory } from '@tanstack/react-router';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { QueryFormData, JsonObject } from '@superset-ui/core';
|
||||
import {
|
||||
@@ -60,7 +60,7 @@ interface ExploreActions {
|
||||
saveFaveStar: (sliceId: number, isStarred: boolean) => void;
|
||||
redirectSQLLab: (
|
||||
formData: QueryFormData,
|
||||
history?: RouterHistory | false,
|
||||
history?: ReturnType<typeof useHistory> | false,
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -187,14 +187,14 @@ const ExploreChartHeader: FC<ExploreChartHeaderProps> = ({
|
||||
setCurrentReportDeleting(null);
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const { redirectSQLLab } = actions;
|
||||
|
||||
const redirectToSQLLab = useCallback(
|
||||
(redirectFormData: QueryFormData, openNewWindow = false) => {
|
||||
redirectSQLLab(redirectFormData, !openNewWindow && router.history);
|
||||
redirectSQLLab(redirectFormData, !openNewWindow && history);
|
||||
},
|
||||
[redirectSQLLab, router],
|
||||
[redirectSQLLab, history],
|
||||
);
|
||||
|
||||
const [menu, isDropdownVisible, setIsDropdownVisible, streamingExportState] =
|
||||
|
||||
@@ -26,10 +26,8 @@ import {
|
||||
VizType,
|
||||
} from '@superset-ui/core';
|
||||
import { QUERY_MODE_REQUISITES } from 'src/explore/constants';
|
||||
import {
|
||||
createMemoryHistory,
|
||||
type RouterHistory,
|
||||
} from '@tanstack/react-router';
|
||||
import { Router, Route } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
@@ -42,17 +40,6 @@ import reducerIndex from 'spec/helpers/reducerIndex';
|
||||
import * as exploreActions from 'src/explore/actions/exploreActions';
|
||||
import ExploreViewContainer from '.';
|
||||
|
||||
// The component syncs the explore URL through `useRouter().history`;
|
||||
// back it with a spy-able in-memory history per test.
|
||||
let mockRouterHistory: RouterHistory | undefined;
|
||||
|
||||
jest.mock('@tanstack/react-router', () => ({
|
||||
...jest.requireActual('@tanstack/react-router'),
|
||||
useRouter: () => ({
|
||||
history: mockRouterHistory,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.doMock('@superset-ui/core', () => ({
|
||||
__esModule: true,
|
||||
...jest.requireActual('@superset-ui/core'),
|
||||
@@ -149,7 +136,7 @@ const renderWithRouter = ({
|
||||
overridePathname?: string;
|
||||
initialState?: object;
|
||||
store?: Store;
|
||||
history?: RouterHistory;
|
||||
history?: ReturnType<typeof createMemoryHistory>;
|
||||
} = {}) => {
|
||||
const path = overridePathname ?? defaultPath;
|
||||
jest.spyOn(window, 'location', 'get').mockReturnValue({
|
||||
@@ -159,15 +146,14 @@ const renderWithRouter = ({
|
||||
const history =
|
||||
existingHistory ??
|
||||
createMemoryHistory({ initialEntries: [`${path}${search}`] });
|
||||
mockRouterHistory = history;
|
||||
const result = render(<ExploreViewContainer />, {
|
||||
useRedux: true,
|
||||
useDnd: true,
|
||||
initialState,
|
||||
store,
|
||||
useRouter: true,
|
||||
initialEntries: [`${path}${search}`],
|
||||
});
|
||||
const result = render(
|
||||
<Router history={history}>
|
||||
<Route path={path}>
|
||||
<ExploreViewContainer />
|
||||
</Route>
|
||||
</Router>,
|
||||
{ useRedux: true, useDnd: true, initialState, store },
|
||||
);
|
||||
return { ...result, history };
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ import { t } from '@apache-superset/core/translation';
|
||||
import { logging } from '@apache-superset/core/utils';
|
||||
import { debounce, isEqual, isObjectLike, omit, pick } from 'lodash';
|
||||
import { Resizable } from 're-resizable';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Tooltip } from '@superset-ui/core/components';
|
||||
import { usePluginContext } from 'src/components';
|
||||
import { Global } from '@emotion/react';
|
||||
@@ -387,7 +387,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
|
||||
getSidebarWidths(LocalStorageKeys.DatasourceWidth),
|
||||
);
|
||||
const tabId = useTabId();
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -477,7 +477,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
|
||||
props.force,
|
||||
title,
|
||||
tabId,
|
||||
router.history,
|
||||
history,
|
||||
);
|
||||
},
|
||||
[
|
||||
@@ -488,7 +488,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
|
||||
props.standalone,
|
||||
props.force,
|
||||
tabId,
|
||||
router,
|
||||
history,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint camelcase: 0 */
|
||||
import { ChangeEvent, ComponentProps, FormEvent, Component } from 'react';
|
||||
import { ChangeEvent, FormEvent, Component } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { nanoid } from 'nanoid';
|
||||
import rison from 'rison';
|
||||
import { connect } from 'react-redux';
|
||||
import { useRouter, type RouterHistory } from '@tanstack/react-router';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import {
|
||||
InfoTooltip,
|
||||
Button,
|
||||
@@ -64,8 +64,7 @@ import { CHART_WIDTH, CHART_HEIGHT } from 'src/dashboard/constants';
|
||||
// Session storage key for recent dashboard
|
||||
const SK_DASHBOARD_ID = 'save_chart_recent_dashboard';
|
||||
|
||||
interface SaveModalProps {
|
||||
history: RouterHistory;
|
||||
interface SaveModalProps extends RouteComponentProps {
|
||||
addDangerToast: (msg: string) => void;
|
||||
actions: Record<string, any>;
|
||||
form_data?: Record<string, any>;
|
||||
@@ -837,18 +836,7 @@ function mapStateToProps({
|
||||
};
|
||||
}
|
||||
|
||||
const ConnectedSaveModal = connect(mapStateToProps)(withTheme(SaveModal));
|
||||
|
||||
// Function wrapper replacing react-router's withRouter HOC: injects the
|
||||
// router history into the class component as an explicit prop.
|
||||
function SaveModalWithRouter(
|
||||
props: Omit<ComponentProps<typeof ConnectedSaveModal>, 'history'>,
|
||||
) {
|
||||
const router = useRouter();
|
||||
return <ConnectedSaveModal {...props} history={router.history} />;
|
||||
}
|
||||
|
||||
export default SaveModalWithRouter;
|
||||
export default withRouter(connect(mapStateToProps)(withTheme(SaveModal)));
|
||||
|
||||
// User for testing purposes need to revisit once we convert this to functional component
|
||||
export { SaveModal as PureSaveModal };
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useLocation } from '@tanstack/react-router';
|
||||
import { Route } from 'react-router-dom';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { DatasourceType, JsonObject, SupersetClient } from '@superset-ui/core';
|
||||
import {
|
||||
@@ -315,19 +315,16 @@ test('Edit dataset should be disabled when user is not admin', async () => {
|
||||
test('Click on View in SQL Lab', async () => {
|
||||
const props = createProps();
|
||||
|
||||
// Renders the current location state once the router navigates to /sqllab,
|
||||
// mimicking the former react-router <Route path="/sqllab" render={...} />.
|
||||
const MockSqlLabRoute = () => {
|
||||
const location = useLocation();
|
||||
if (location.pathname !== '/sqllab') return null;
|
||||
return (
|
||||
<div data-test="mock-sqllab-route">{JSON.stringify(location.state)}</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { queryByTestId, findByTestId, getByTestId } = render(
|
||||
const { queryByTestId, getByTestId } = render(
|
||||
<>
|
||||
<MockSqlLabRoute />
|
||||
<Route
|
||||
path="/sqllab"
|
||||
render={({ location }) => (
|
||||
<div data-test="mock-sqllab-route">
|
||||
{JSON.stringify(location.state)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<DatasourceControl {...props} />
|
||||
</>,
|
||||
{
|
||||
@@ -341,14 +338,14 @@ test('Click on View in SQL Lab', async () => {
|
||||
|
||||
await userEvent.click(screen.getByText('View in SQL Lab'));
|
||||
|
||||
expect(await findByTestId('mock-sqllab-route')).toBeInTheDocument();
|
||||
expect(getByTestId('mock-sqllab-route')).toBeInTheDocument();
|
||||
expect(JSON.parse(`${getByTestId('mock-sqllab-route').textContent}`)).toEqual(
|
||||
expect.objectContaining({
|
||||
{
|
||||
requestedQuery: {
|
||||
datasourceKey: `${mockDatasource.id}__${mockDatasource.type}`,
|
||||
sql: mockDatasource.sql,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ import ViewQueryModalFooter from 'src/explore/components/controls/ViewQueryModal
|
||||
import ViewQuery from 'src/explore/components/controls/ViewQuery';
|
||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { safeStringify } from 'src/utils/safeStringify';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// Extended Datasource interface with all properties used in this component
|
||||
interface ExtendedDatasource extends Datasource {
|
||||
@@ -415,8 +415,10 @@ class DatasourceControl extends PureComponent<
|
||||
key: VIEW_IN_SQL_LAB,
|
||||
label: (
|
||||
<Link
|
||||
to="/sqllab"
|
||||
state={{ requestedQuery }}
|
||||
to={{
|
||||
pathname: '/sqllab',
|
||||
state: { requestedQuery },
|
||||
}}
|
||||
onClick={preventRouterLinkWhileMetaClicked}
|
||||
>
|
||||
{t('View in SQL Lab')}
|
||||
@@ -470,8 +472,10 @@ class DatasourceControl extends PureComponent<
|
||||
key: VIEW_IN_SQL_LAB,
|
||||
label: (
|
||||
<Link
|
||||
to="/sqllab"
|
||||
state={{ requestedQuery }}
|
||||
to={{
|
||||
pathname: '/sqllab',
|
||||
state: { requestedQuery },
|
||||
}}
|
||||
onClick={preventRouterLinkWhileMetaClicked}
|
||||
>
|
||||
{t('View in SQL Lab')}
|
||||
|
||||
@@ -29,12 +29,10 @@ import { RootState } from 'src/dashboard/types';
|
||||
import ViewQuery, { ViewQueryProps } from './ViewQuery';
|
||||
|
||||
const mockHistoryPush = jest.fn();
|
||||
jest.mock('@tanstack/react-router', () => ({
|
||||
...jest.requireActual('@tanstack/react-router'),
|
||||
useRouter: () => ({
|
||||
history: {
|
||||
push: mockHistoryPush,
|
||||
},
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: () => ({
|
||||
push: mockHistoryPush,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -164,10 +162,13 @@ test('navigates to SQL Lab when View in SQL Lab button is clicked', () => {
|
||||
const viewInSQLLabButton = screen.getByText('View in SQL Lab');
|
||||
fireEvent.click(viewInSQLLabButton);
|
||||
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith('/sqllab', {
|
||||
requestedQuery: {
|
||||
datasourceKey: mockProps.datasource,
|
||||
sql: mockProps.sql,
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith({
|
||||
pathname: '/sqllab',
|
||||
state: {
|
||||
requestedQuery: {
|
||||
datasourceKey: mockProps.datasource,
|
||||
sql: mockProps.sql,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,8 +45,7 @@ import CodeSyntaxHighlighter, {
|
||||
SupportedLanguage,
|
||||
preloadLanguages,
|
||||
} from '@superset-ui/core/components/CodeSyntaxHighlighter';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ExplorePageState } from 'src/explore/types';
|
||||
|
||||
export interface ViewQueryProps {
|
||||
@@ -87,7 +86,7 @@ const ViewQuery: FC<ViewQueryProps> = props => {
|
||||
);
|
||||
const [formattedSQL, setFormattedSQL] = useState<string>();
|
||||
const [showFormatSQL, setShowFormatSQL] = useState(true);
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const currentSQL = (showFormatSQL ? formattedSQL : sql) ?? sql;
|
||||
const canAccessSQLLab = useSelector((state: RootState) =>
|
||||
findPermission('menu_access', 'SQL Lab', state.user?.roles),
|
||||
@@ -148,10 +147,10 @@ const ViewQuery: FC<ViewQueryProps> = props => {
|
||||
'_blank',
|
||||
);
|
||||
} else {
|
||||
pushAppHref(router, '/sqllab', { requestedQuery });
|
||||
history.push({ pathname: '/sqllab', state: { requestedQuery } });
|
||||
}
|
||||
},
|
||||
[router, datasource, currentSQL],
|
||||
[history, datasource, currentSQL],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -21,8 +21,7 @@ import { isObject } from 'lodash';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { Button } from '@superset-ui/core/components';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
interface SimpleDataSource {
|
||||
id: string;
|
||||
@@ -45,7 +44,7 @@ const ViewQueryModalFooter: FC<ViewQueryModalFooterProps> = (props: {
|
||||
changeDatasource: () => void;
|
||||
datasource: SimpleDataSource;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const viewInSQLLab = (
|
||||
openInNewWindow: boolean,
|
||||
id: string,
|
||||
@@ -59,8 +58,11 @@ const ViewQueryModalFooter: FC<ViewQueryModalFooterProps> = (props: {
|
||||
if (openInNewWindow) {
|
||||
SupersetClient.postForm('/sqllab/', payload);
|
||||
} else {
|
||||
pushAppHref(router, '/sqllab', {
|
||||
requestedQuery: payload,
|
||||
history.push({
|
||||
pathname: '/sqllab',
|
||||
state: {
|
||||
requestedQuery: payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import { t } from '@apache-superset/core/translation';
|
||||
import { css, useTheme } from '@apache-superset/core/theme';
|
||||
import { MenuItem } from '@superset-ui/core/components/Menu';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export interface DashboardsMenuProps {
|
||||
chartId?: number;
|
||||
@@ -45,10 +45,7 @@ export const useDashboardsMenuItems = ({
|
||||
);
|
||||
}, [dashboards, searchTerm]);
|
||||
|
||||
const urlSearch = useMemo(
|
||||
() => (chartId ? { focused_chart: String(chartId) } : undefined),
|
||||
[chartId],
|
||||
);
|
||||
const urlQueryString = chartId ? `?focused_chart=${chartId}` : '';
|
||||
const noResults = dashboards.length === 0;
|
||||
const noResultsFound = searchTerm && filteredDashboards.length === 0;
|
||||
|
||||
@@ -75,8 +72,7 @@ export const useDashboardsMenuItems = ({
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noreferer noopener"
|
||||
to={`/superset/dashboard/${dashboard.id}`}
|
||||
search={urlSearch}
|
||||
to={`/superset/dashboard/${dashboard.id}${urlQueryString}`}
|
||||
css={css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -106,7 +102,7 @@ export const useDashboardsMenuItems = ({
|
||||
return items;
|
||||
}, [
|
||||
filteredDashboards,
|
||||
urlSearch,
|
||||
urlQueryString,
|
||||
noResults,
|
||||
noResultsFound,
|
||||
theme.sizeUnit,
|
||||
|
||||
@@ -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}</>;
|
||||
};
|
||||
|
||||
@@ -16,13 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ReactNode } from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
|
||||
import { css } from '@apache-superset/core/theme';
|
||||
import { Link, useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { parseSearch } from 'src/router/searchParams';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import {
|
||||
ConfirmStatusChange,
|
||||
Button,
|
||||
@@ -58,20 +55,6 @@ interface ChartCardProps {
|
||||
getData?: (tab: TableTab) => void;
|
||||
}
|
||||
|
||||
// Backend-provided chart URLs may carry a query string; split it out so
|
||||
// the router preserves it via the raw search codec.
|
||||
function CardLink({ to, children }: { to: string; children?: ReactNode }) {
|
||||
const [pathname, queryString] = to.split('?');
|
||||
return (
|
||||
<Link
|
||||
to={pathname}
|
||||
{...(queryString ? { search: parseSearch(queryString) } : {})}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ChartCard({
|
||||
chart,
|
||||
hasPerm,
|
||||
@@ -89,7 +72,7 @@ export default function ChartCard({
|
||||
handleBulkChartExport,
|
||||
getData,
|
||||
}: ChartCardProps) {
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const canEdit = hasPerm('can_write');
|
||||
const canDelete = hasPerm('can_write');
|
||||
const canExport = hasPerm('can_export');
|
||||
@@ -187,7 +170,7 @@ export default function ChartCard({
|
||||
<CardStyles
|
||||
onClick={() => {
|
||||
if (!bulkSelectEnabled && chart.url) {
|
||||
pushAppHref(router, chart.url);
|
||||
history.push(chart.url);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -209,7 +192,7 @@ export default function ChartCard({
|
||||
description={t('Modified %s', chart.changed_on_delta_humanized)}
|
||||
coverLeft={<FacePile users={chart.owners || []} />}
|
||||
coverRight={<Label>{chart.datasource_name_text}</Label>}
|
||||
linkComponent={CardLink}
|
||||
linkComponent={Link}
|
||||
actions={
|
||||
<ListViewCard.Actions
|
||||
onClick={e => {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
JsonResponse,
|
||||
SupersetClient,
|
||||
@@ -68,7 +68,7 @@ afterAll(() => {
|
||||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<StandaloneRouter>
|
||||
<MemoryRouter>
|
||||
<DashboardCard
|
||||
dashboard={mockDashboard}
|
||||
hasPerm={mockHasPerm}
|
||||
@@ -80,7 +80,7 @@ beforeEach(() => {
|
||||
handleBulkDashboardExport={mockHandleBulkDashboardExport}
|
||||
onDelete={mockOnDelete}
|
||||
/>
|
||||
</StandaloneRouter>,
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -129,7 +129,6 @@ test('should fetch thumbnail when dashboard has no thumbnail URL and feature fla
|
||||
handleBulkDashboardExport={() => {}}
|
||||
onDelete={() => {}}
|
||||
/>,
|
||||
{ useRouter: true },
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockGet).toHaveBeenCalledWith({
|
||||
|
||||
@@ -16,10 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import { Link, useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { parseSearch } from 'src/router/searchParams';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
isFeatureEnabled,
|
||||
@@ -55,20 +53,6 @@ interface DashboardCardProps {
|
||||
onDelete: (dashboard: Dashboard) => void;
|
||||
}
|
||||
|
||||
// Backend-provided dashboard URLs may carry a query string; split it out
|
||||
// so the router preserves it via the raw search codec.
|
||||
function CardLink({ to, children }: { to: string; children?: ReactNode }) {
|
||||
const [pathname, queryString] = to.split('?');
|
||||
return (
|
||||
<Link
|
||||
to={pathname}
|
||||
{...(queryString ? { search: parseSearch(queryString) } : {})}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardCard({
|
||||
dashboard,
|
||||
hasPerm,
|
||||
@@ -81,7 +65,7 @@ function DashboardCard({
|
||||
handleBulkDashboardExport,
|
||||
onDelete,
|
||||
}: DashboardCardProps) {
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const canEdit = hasPerm('can_write');
|
||||
const canDelete = hasPerm('can_write');
|
||||
const canExport = hasPerm('can_export');
|
||||
@@ -170,7 +154,7 @@ function DashboardCard({
|
||||
<CardStyles
|
||||
onClick={() => {
|
||||
if (!bulkSelectEnabled) {
|
||||
pushAppHref(router, dashboard.url);
|
||||
history.push(dashboard.url);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -186,7 +170,7 @@ function DashboardCard({
|
||||
) : null
|
||||
}
|
||||
url={bulkSelectEnabled ? undefined : dashboard.url}
|
||||
linkComponent={CardLink}
|
||||
linkComponent={Link}
|
||||
imgURL={thumbnailUrl}
|
||||
imgFallbackURL={assetUrl(
|
||||
'/static/assets/images/dashboard-card-fallback.svg',
|
||||
|
||||
@@ -45,12 +45,10 @@ jest.mock('@superset-ui/core', () => ({
|
||||
}));
|
||||
|
||||
const mockHistoryPush = jest.fn();
|
||||
jest.mock('@tanstack/react-router', () => ({
|
||||
...jest.requireActual('@tanstack/react-router'),
|
||||
useRouter: () => ({
|
||||
history: {
|
||||
push: mockHistoryPush,
|
||||
},
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: () => ({
|
||||
push: mockHistoryPush,
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -33,8 +33,7 @@ import {
|
||||
} from 'react';
|
||||
import { CheckboxChangeEvent } from '@superset-ui/core/components/Checkbox/types';
|
||||
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||
import Tabs from '@superset-ui/core/components/Tabs';
|
||||
import {
|
||||
@@ -752,7 +751,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
(DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
|
||||
)?.parameters !== undefined;
|
||||
const showDBError = validationErrors || dbErrors;
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
|
||||
const dbModel: DatabaseForm =
|
||||
// TODO: we need a centralized engine in one place
|
||||
@@ -888,7 +887,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
};
|
||||
|
||||
const redirectURL = (url: string) => {
|
||||
pushAppHref(router, url);
|
||||
history.push(url);
|
||||
};
|
||||
|
||||
// Database import logic
|
||||
@@ -1876,8 +1875,8 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
fetchAndSetDB();
|
||||
// redirectURL() prefixes the application root via pushAppHref,
|
||||
// so pass a root-relative path.
|
||||
// redirectURL() delegates to history.push; React Router's basename
|
||||
// already prefixes the application root, so pass a relative path.
|
||||
redirectURL('/sqllab?db=true');
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { EmptyState } from '@superset-ui/core/components';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.sizeUnit * 8}px
|
||||
|
||||
@@ -24,16 +24,11 @@ import {
|
||||
} from 'spec/helpers/testing-library';
|
||||
import Footer from 'src/features/datasets/AddDataset/Footer';
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
const mockHistoryPush = jest.fn();
|
||||
jest.mock('@tanstack/react-router', () => ({
|
||||
...jest.requireActual('@tanstack/react-router'),
|
||||
useNavigate: () => mockNavigate,
|
||||
useRouter: () => ({
|
||||
history: {
|
||||
push: mockHistoryPush,
|
||||
back: jest.fn(),
|
||||
},
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: () => ({
|
||||
push: mockHistoryPush,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -178,9 +173,7 @@ describe('Footer', () => {
|
||||
schema: 'public',
|
||||
table_name: 'real_info',
|
||||
});
|
||||
expect(mockNavigate).toHaveBeenCalledWith({
|
||||
to: '/tablemodelview/list/',
|
||||
});
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith('/tablemodelview/list/');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,7 +192,6 @@ describe('Footer', () => {
|
||||
expect(mockCreateResource).toHaveBeenCalled();
|
||||
// Should not navigate if creation failed
|
||||
expect(mockHistoryPush).not.toHaveBeenCalled();
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useNavigate, useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
DropdownButton,
|
||||
@@ -62,8 +61,7 @@ function Footer({
|
||||
hasColumns = false,
|
||||
datasets,
|
||||
}: FooterProps) {
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const theme = useTheme();
|
||||
const { createResource, state } = useSingleViewResource<
|
||||
Partial<DatasetObject>
|
||||
@@ -89,7 +87,7 @@ function Footer({
|
||||
const logAction = createLogAction(datasetObject);
|
||||
logEvent(logAction, datasetObject);
|
||||
}
|
||||
router.history.back();
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
const tooltipText = t('Select a database table.');
|
||||
@@ -110,12 +108,9 @@ function Footer({
|
||||
logEvent(LOG_ACTIONS_DATASET_CREATION_SUCCESS, datasetObject);
|
||||
// When a dataset is created the response we get is its ID number
|
||||
if (createChart) {
|
||||
pushAppHref(
|
||||
router,
|
||||
`/chart/add/?dataset=${datasetObject.table_name}`,
|
||||
);
|
||||
history.push(`/chart/add/?dataset=${datasetObject.table_name}`);
|
||||
} else {
|
||||
navigate({ to: '/tablemodelview/list/' });
|
||||
history.push('/tablemodelview/list/');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -25,6 +25,14 @@ import DatasetPanelComponent from 'src/features/datasets/AddDataset/DatasetPanel
|
||||
import RightPanel from 'src/features/datasets/AddDataset/RightPanel';
|
||||
import Footer from 'src/features/datasets/AddDataset/Footer';
|
||||
|
||||
const mockHistoryPush = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: () => ({
|
||||
push: mockHistoryPush,
|
||||
}),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('DatasetLayout', () => {
|
||||
test('renders nothing when no components are passed in', () => {
|
||||
|
||||
@@ -21,8 +21,7 @@ import { extendedDayjs } from '@superset-ui/core/utils/dates';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { parseSearch } from 'src/router/searchParams';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ListViewCard } from '@superset-ui/core/components';
|
||||
import { Dashboard, SavedQueryObject, TableTab } from 'src/views/CRUD/types';
|
||||
import { ActivityData, LoadingCards } from 'src/pages/Home';
|
||||
@@ -187,14 +186,9 @@ export default function ActivityTable({
|
||||
return activities.map((entity: ActivityObject) => {
|
||||
const url = getEntityUrl(entity);
|
||||
const lastActionOn = getEntityLastActionOn(entity);
|
||||
// Entity URLs come from backend data and may carry a query string.
|
||||
const [pathname, queryString] = (url || '').split('?');
|
||||
return (
|
||||
<CardStyles key={url}>
|
||||
<Link
|
||||
to={pathname}
|
||||
search={queryString ? parseSearch(queryString) : undefined}
|
||||
>
|
||||
<Link to={url}>
|
||||
<ListViewCard
|
||||
cover={<></>}
|
||||
url={url}
|
||||
|
||||
@@ -29,8 +29,7 @@ import {
|
||||
setItem,
|
||||
} from 'src/utils/localStorageHelpers';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Filter, TableTab } from 'src/views/CRUD/types';
|
||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||
import { User } from 'src/types/bootstrapTypes';
|
||||
@@ -72,7 +71,7 @@ function ChartTable({
|
||||
otherTabFilters,
|
||||
otherTabTitle,
|
||||
}: ChartTableProps) {
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const initialTab = getItem(
|
||||
LocalStorageKeys.HomepageChartFilter,
|
||||
TableTab.Other,
|
||||
@@ -216,7 +215,7 @@ function ChartTable({
|
||||
'Yes',
|
||||
)},value:!t))`
|
||||
: '/chart/list/';
|
||||
pushAppHref(router, target);
|
||||
history.push(target);
|
||||
},
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import * as hooks from 'src/views/CRUD/hooks';
|
||||
@@ -104,6 +105,7 @@ const defaultProps = {
|
||||
otherTabTitle: 'Examples',
|
||||
};
|
||||
|
||||
const history = createMemoryHistory();
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
dashboards: (state = { dashboards: [] }) => state,
|
||||
@@ -154,9 +156,9 @@ beforeEach(() => {
|
||||
|
||||
test('renders loading state initially', () => {
|
||||
render(
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<Router history={history}>
|
||||
<DashboardTable {...defaultProps} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ store },
|
||||
);
|
||||
expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument();
|
||||
@@ -164,9 +166,9 @@ test('renders loading state initially', () => {
|
||||
|
||||
test('renders empty state when no dashboards', async () => {
|
||||
render(
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<Router history={history}>
|
||||
<DashboardTable {...defaultProps} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
@@ -192,9 +194,9 @@ test('renders dashboard cards when data is loaded', async () => {
|
||||
}));
|
||||
|
||||
render(
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<Router history={history}>
|
||||
<DashboardTable {...defaultProps} mine={mockDashboards} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
@@ -212,9 +214,9 @@ test('switches to Mine tab correctly', async () => {
|
||||
};
|
||||
|
||||
render(
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<Router history={history}>
|
||||
<DashboardTable {...props} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
@@ -233,9 +235,9 @@ test('handles create dashboard button click', async () => {
|
||||
} as Location);
|
||||
|
||||
render(
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<Router history={history}>
|
||||
<DashboardTable {...defaultProps} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
@@ -253,9 +255,9 @@ test('switches to Other tab when available', async () => {
|
||||
};
|
||||
|
||||
render(
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<Router history={history}>
|
||||
<DashboardTable {...props} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
@@ -297,9 +299,9 @@ test('handles bulk dashboard export with correct ID and shows spinner', async ()
|
||||
}));
|
||||
|
||||
render(
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<Router history={history}>
|
||||
<DashboardTable {...props} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
@@ -360,9 +362,9 @@ test('handles dashboard deletion confirmation', async () => {
|
||||
}));
|
||||
|
||||
render(
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<Router history={history}>
|
||||
<DashboardTable {...props} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
@@ -432,9 +434,9 @@ test('passes correct parameters to handleDashboardDelete for Other tab', async (
|
||||
};
|
||||
|
||||
render(
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<Router history={history}>
|
||||
<DashboardTable {...props} />
|
||||
</StandaloneRouter>,
|
||||
</Router>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@ import { SupersetClient } from '@superset-ui/core';
|
||||
import { useFavoriteStatus, useListViewResource } from 'src/views/CRUD/hooks';
|
||||
import { Dashboard, DashboardTableProps, TableTab } from 'src/views/CRUD/types';
|
||||
import handleResourceExport from 'src/utils/export';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { pushAppHref } from 'src/router/navigation';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
getItem,
|
||||
LocalStorageKeys,
|
||||
@@ -57,7 +56,7 @@ function DashboardTable({
|
||||
otherTabFilters,
|
||||
otherTabTitle,
|
||||
}: DashboardTableProps) {
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
const defaultTab = getItem(
|
||||
LocalStorageKeys.HomepageDashboardFilter,
|
||||
TableTab.Other,
|
||||
@@ -217,7 +216,7 @@ function DashboardTable({
|
||||
'Yes',
|
||||
)},value:!t))`
|
||||
: '/dashboard/list/';
|
||||
pushAppHref(router, target);
|
||||
history.push(target);
|
||||
},
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -25,8 +25,7 @@ import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { MainNav, MenuItem } from '@superset-ui/core/components/Menu';
|
||||
import { Tooltip, Grid, Row, Col, Image } from '@superset-ui/core/components';
|
||||
import { GenericLink } from 'src/components';
|
||||
import { Link, useLocation } from '@tanstack/react-router';
|
||||
import { parseSearch } from 'src/router/searchParams';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { Typography } from '@superset-ui/core/components/Typography';
|
||||
import { useUiConfig } from 'src/components/UiConfigContext';
|
||||
@@ -245,19 +244,12 @@ export function Menu({
|
||||
isFrontendRoute,
|
||||
}: MenuObjectProps): MenuItem => {
|
||||
if (url && isFrontendRoute) {
|
||||
// Menu URLs come from backend data and may carry a query string.
|
||||
const [pathname, queryString] = url.split('?');
|
||||
return {
|
||||
key: label,
|
||||
label: (
|
||||
<Link
|
||||
role="button"
|
||||
to={pathname}
|
||||
search={queryString ? parseSearch(queryString) : undefined}
|
||||
activeProps={{ className: 'is-active' }}
|
||||
>
|
||||
<NavLink role="button" to={url} activeClassName="is-active">
|
||||
{label}
|
||||
</Link>
|
||||
</NavLink>
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -274,20 +266,12 @@ export function Menu({
|
||||
if (typeof child === 'string' && child === '-' && label !== 'Data') {
|
||||
childItems.push({ type: 'divider', key: `divider-${index1}` });
|
||||
} else if (typeof child !== 'string') {
|
||||
const [childPathname, childQueryString] = (child.url || '').split('?');
|
||||
childItems.push({
|
||||
key: `${child.label}`,
|
||||
label: child.isFrontendRoute ? (
|
||||
<Link
|
||||
to={childPathname}
|
||||
search={
|
||||
childQueryString ? parseSearch(childQueryString) : undefined
|
||||
}
|
||||
activeOptions={{ exact: true }}
|
||||
activeProps={{ className: 'is-active' }}
|
||||
>
|
||||
<NavLink to={child.url || ''} exact activeClassName="is-active">
|
||||
{child.label}
|
||||
</Link>
|
||||
</NavLink>
|
||||
) : (
|
||||
<Typography.Link href={child.url}>{child.label}</Typography.Link>
|
||||
),
|
||||
|
||||
@@ -16,18 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
FC,
|
||||
PureComponent,
|
||||
ReactNode,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { useState, useEffect, FC, PureComponent, useMemo } from 'react';
|
||||
import rison from 'rison';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { parseSearch } from 'src/router/searchParams';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useQueryParams, BooleanParam } from 'use-query-params';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
@@ -110,26 +102,6 @@ const StyledMenuItem = styled.div<{ disabled?: boolean }>`
|
||||
`}
|
||||
`;
|
||||
|
||||
// Menu URLs may carry a query string (e.g. /sqllab?new=true); the TanStack
|
||||
// <Link> needs the search params passed separately from the pathname.
|
||||
const RouterLink = ({
|
||||
url,
|
||||
children,
|
||||
}: {
|
||||
url: string;
|
||||
children?: ReactNode;
|
||||
}) => {
|
||||
const [pathname, queryString] = url.split('?');
|
||||
return (
|
||||
<Link
|
||||
to={pathname}
|
||||
search={queryString ? parseSearch(queryString) : undefined}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const RightMenu = ({
|
||||
align,
|
||||
settings,
|
||||
@@ -437,7 +409,7 @@ const RightMenu = ({
|
||||
items.push({
|
||||
key: menu.label,
|
||||
label: isFrontendRoute(menu.url) ? (
|
||||
<RouterLink url={menu.url || ''}>{menu.label}</RouterLink>
|
||||
<Link to={menu.url || ''}>{menu.label}</Link>
|
||||
) : (
|
||||
<Typography.Link href={ensureAppRoot(menu.url || '')}>
|
||||
{menu.label}
|
||||
@@ -453,7 +425,7 @@ const RightMenu = ({
|
||||
items.push({
|
||||
key: menu.label,
|
||||
label: isFrontendRoute(menu.url) ? (
|
||||
<RouterLink url={menu.url || ''}>{menu.label}</RouterLink>
|
||||
<Link to={menu.url || ''}>{menu.label}</Link>
|
||||
) : (
|
||||
<Typography.Link href={ensureAppRoot(menu.url || '')}>
|
||||
{menu.label}
|
||||
@@ -488,7 +460,7 @@ const RightMenu = ({
|
||||
sectionItems.push({
|
||||
key: child.label,
|
||||
label: isFrontendRoute(child.url) ? (
|
||||
<RouterLink url={child.url || ''}>{menuItemDisplay}</RouterLink>
|
||||
<Link to={child.url || ''}>{menuItemDisplay}</Link>
|
||||
) : (
|
||||
<Typography.Link
|
||||
href={child.url || ''}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { styled, useTheme, css } from '@apache-superset/core/theme';
|
||||
@@ -206,11 +206,7 @@ export const SavedQueries = ({
|
||||
if (canEdit) {
|
||||
menuItems.push({
|
||||
key: 'edit',
|
||||
label: (
|
||||
<Link to="/sqllab" search={{ savedQueryId: String(query.id) }}>
|
||||
{t('Edit')}
|
||||
</Link>
|
||||
),
|
||||
label: <Link to={`/sqllab?savedQueryId=${query.id}`}>{t('Edit')}</Link>,
|
||||
});
|
||||
}
|
||||
menuItems.push({
|
||||
@@ -282,8 +278,7 @@ export const SavedQueries = ({
|
||||
icon: <Icons.PlusOutlined iconSize="m" />,
|
||||
name: (
|
||||
<Link
|
||||
to="/sqllab"
|
||||
search={{ new: 'true' }}
|
||||
to="/sqllab?new=true"
|
||||
css={css`
|
||||
&:hover {
|
||||
color: currentColor;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import SubMenu, { ButtonProps } from './SubMenu';
|
||||
|
||||
@@ -63,9 +63,9 @@ const setup = (overrides: Record<string, any> = {}) => {
|
||||
...overrides,
|
||||
};
|
||||
return render(
|
||||
<StandaloneRouter>
|
||||
<BrowserRouter>
|
||||
<SubMenu {...props} />
|
||||
</StandaloneRouter>,
|
||||
</BrowserRouter>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
*/
|
||||
import { ReactNode, useState, useEffect, FunctionComponent } from 'react';
|
||||
|
||||
import { Link, useRouter } from '@tanstack/react-router';
|
||||
import { parseSearch } from 'src/router/searchParams';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
styled,
|
||||
@@ -173,9 +172,14 @@ const SubMenuComponent: FunctionComponent<SubMenuProps> = props => {
|
||||
const [navRightStyle, setNavRightStyle] = useState('nav-right');
|
||||
const theme = useTheme();
|
||||
|
||||
// If no parent <RouterProvider> exists, useRouter returns undefined and
|
||||
// we know not to use <Link> in render
|
||||
const hasHistory = !!useRouter({ warn: false });
|
||||
let hasHistory = true;
|
||||
// If no parent <Router> component exists, useHistory throws an error
|
||||
try {
|
||||
useHistory();
|
||||
} catch (err) {
|
||||
// If error is thrown, we know not to use <Link> in render
|
||||
hasHistory = false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
@@ -219,14 +223,11 @@ const SubMenuComponent: FunctionComponent<SubMenuProps> = props => {
|
||||
role="tablist"
|
||||
items={props.tabs?.map(tab => {
|
||||
if ((props.usesRouter || hasHistory) && !!tab.usesRouter) {
|
||||
// Tab URLs may carry a query string.
|
||||
const [pathname, queryString] = (tab.url || '').split('?');
|
||||
return {
|
||||
key: tab.label,
|
||||
label: (
|
||||
<Link
|
||||
to={pathname}
|
||||
search={queryString ? parseSearch(queryString) : undefined}
|
||||
to={tab.url || ''}
|
||||
role="tab"
|
||||
id={tab.id || tab.name}
|
||||
data-test={tab['data-test']}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
|
||||
import { CardStyles } from 'src/views/CRUD/utils';
|
||||
|
||||
@@ -18,16 +18,10 @@
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { getClientErrorObject } from '@superset-ui/core';
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
useState,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from 'react';
|
||||
import { useBlocker } from '@tanstack/react-router';
|
||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useBeforeUnload } from 'src/hooks/useBeforeUnload';
|
||||
import type { Location, Action } from 'history';
|
||||
|
||||
type UseUnsavedChangesPromptProps = {
|
||||
hasUnsavedChanges: boolean;
|
||||
@@ -42,53 +36,16 @@ export const useUnsavedChangesPrompt = ({
|
||||
isSaveModalVisible = false,
|
||||
manualSaveOnUnsavedChanges = false,
|
||||
}: UseUnsavedChangesPromptProps) => {
|
||||
const [showModal, setShowModalState] = useState(false);
|
||||
const showModalRef = useRef(showModal);
|
||||
showModalRef.current = showModal;
|
||||
const history = useHistory();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const confirmNavigationRef = useRef<(() => void) | null>(null);
|
||||
const unblockRef = useRef<() => void>(() => {});
|
||||
const manualSaveRef = useRef(false); // Track if save was user-initiated (not via navigation)
|
||||
|
||||
const blocker = useBlocker({
|
||||
shouldBlockFn: ({ action }) => {
|
||||
// REPLACE actions are URL sync (e.g. updating form_data_key), not navigation
|
||||
if (action === 'REPLACE') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (manualSaveRef.current) {
|
||||
manualSaveRef.current = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
withResolver: true,
|
||||
disabled: !hasUnsavedChanges,
|
||||
// the manual useBeforeUnload listener below handles the unload prompt
|
||||
enableBeforeUnload: false,
|
||||
});
|
||||
const blockerRef = useRef(blocker);
|
||||
blockerRef.current = blocker;
|
||||
|
||||
useEffect(() => {
|
||||
if (blocker.status === 'blocked') {
|
||||
setShowModalState(true);
|
||||
}
|
||||
}, [blocker.status]);
|
||||
|
||||
// Closing the modal without navigating discards the blocked navigation
|
||||
const setShowModal: Dispatch<SetStateAction<boolean>> = useCallback(value => {
|
||||
const next =
|
||||
typeof value === 'function' ? value(showModalRef.current) : value;
|
||||
if (!next) {
|
||||
blockerRef.current.reset?.();
|
||||
}
|
||||
setShowModalState(next);
|
||||
}, []);
|
||||
|
||||
const handleConfirmNavigation = useCallback(() => {
|
||||
setShowModalState(false);
|
||||
blockerRef.current.proceed?.();
|
||||
setShowModal(false);
|
||||
confirmNavigationRef.current?.();
|
||||
}, []);
|
||||
|
||||
const handleSaveAndCloseModal = useCallback(async () => {
|
||||
@@ -106,19 +63,66 @@ export const useUnsavedChangesPrompt = ({
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
}, [manualSaveOnUnsavedChanges, onSave, setShowModal]);
|
||||
}, [manualSaveOnUnsavedChanges, onSave]);
|
||||
|
||||
const triggerManualSave = useCallback(() => {
|
||||
manualSaveRef.current = true;
|
||||
onSave();
|
||||
}, [onSave]);
|
||||
|
||||
const blockCallback = useCallback(
|
||||
(
|
||||
{
|
||||
pathname,
|
||||
search,
|
||||
state,
|
||||
}: {
|
||||
pathname: Location['pathname'];
|
||||
search: Location['search'];
|
||||
state: Location['state'];
|
||||
},
|
||||
action: Action,
|
||||
) => {
|
||||
// REPLACE actions are URL sync (e.g. updating form_data_key), not navigation
|
||||
if (action === 'REPLACE') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (manualSaveRef.current) {
|
||||
manualSaveRef.current = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
confirmNavigationRef.current = () => {
|
||||
unblockRef.current?.();
|
||||
if (action === 'POP') {
|
||||
history.go(-1);
|
||||
} else {
|
||||
history.push({ pathname, search }, state);
|
||||
}
|
||||
};
|
||||
|
||||
setShowModal(true);
|
||||
return false;
|
||||
},
|
||||
[history],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasUnsavedChanges) return undefined;
|
||||
|
||||
const unblock = history.block(blockCallback);
|
||||
unblockRef.current = unblock;
|
||||
|
||||
return () => unblock();
|
||||
}, [blockCallback, hasUnsavedChanges, history]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaveModalVisible && manualSaveRef.current) {
|
||||
setShowModal(false);
|
||||
manualSaveRef.current = false;
|
||||
}
|
||||
}, [isSaveModalVisible, setShowModal]);
|
||||
}, [isSaveModalVisible]);
|
||||
|
||||
useBeforeUnload(hasUnsavedChanges);
|
||||
|
||||
|
||||
@@ -16,143 +16,152 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactNode } from 'react';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { useUnsavedChangesPrompt } from '.';
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<StandaloneRouter initialEntries={['/dashboard']}>
|
||||
{children}
|
||||
</StandaloneRouter>
|
||||
);
|
||||
|
||||
const setup = async ({ onSave = jest.fn() }: { onSave?: jest.Mock } = {}) => {
|
||||
const utils = renderHook(
|
||||
() => ({
|
||||
prompt: useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave,
|
||||
}),
|
||||
router: useRouter(),
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
// the router mounts asynchronously before rendering its children
|
||||
await waitFor(() => expect(utils.result.current).toBeTruthy());
|
||||
return utils;
|
||||
};
|
||||
|
||||
test('should not show modal initially', async () => {
|
||||
const { result } = await setup();
|
||||
|
||||
expect(result.current.prompt.showModal).toBe(false);
|
||||
let history = createMemoryHistory({
|
||||
initialEntries: ['/dashboard'],
|
||||
});
|
||||
|
||||
test('should block navigation and show modal if there are unsaved changes', async () => {
|
||||
const { result } = await setup();
|
||||
beforeEach(() => {
|
||||
history = createMemoryHistory({ initialEntries: ['/dashboard'] });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.router.history.push('/another-page');
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<Router history={history}>{children}</Router>
|
||||
);
|
||||
|
||||
test('should not show modal initially', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave: jest.fn(),
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
|
||||
test('should block navigation and show modal if there are unsaved changes', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave: jest.fn(),
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
history.push('/another-page');
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.prompt.showModal).toBe(true));
|
||||
expect(result.current.router.state.location.pathname).toBe('/dashboard');
|
||||
expect(result.current.showModal).toBe(true);
|
||||
});
|
||||
|
||||
test('should trigger onSave and hide modal on handleSaveAndCloseModal', async () => {
|
||||
const onSave = jest.fn().mockResolvedValue(undefined);
|
||||
const { result } = await setup({ onSave });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.prompt.handleSaveAndCloseModal();
|
||||
});
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await result.current.handleSaveAndCloseModal();
|
||||
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(result.current.prompt.showModal).toBe(false);
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
|
||||
test('should trigger manual save and not show modal again', async () => {
|
||||
const onSave = jest.fn().mockResolvedValue(undefined);
|
||||
const { result } = await setup({ onSave });
|
||||
|
||||
act(() => {
|
||||
result.current.prompt.triggerManualSave();
|
||||
});
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
result.current.triggerManualSave();
|
||||
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(result.current.prompt.showModal).toBe(false);
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
|
||||
test('should close modal when handleConfirmNavigation is called', async () => {
|
||||
const { result } = await setup();
|
||||
test('should close modal when handleConfirmNavigation is called', () => {
|
||||
const onSave = jest.fn();
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// First, trigger navigation to show the modal
|
||||
await act(async () => {
|
||||
result.current.router.history.push('/another-page');
|
||||
act(() => {
|
||||
history.push('/another-page');
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.prompt.showModal).toBe(true));
|
||||
expect(result.current.showModal).toBe(true);
|
||||
|
||||
// Then call handleConfirmNavigation to discard changes
|
||||
await act(async () => {
|
||||
result.current.prompt.handleConfirmNavigation();
|
||||
act(() => {
|
||||
result.current.handleConfirmNavigation();
|
||||
});
|
||||
|
||||
expect(result.current.prompt.showModal).toBe(false);
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
|
||||
test('should preserve pathname, search, and state when confirming navigation', async () => {
|
||||
const { result } = await setup();
|
||||
test('should preserve pathname, search, and state when confirming navigation', () => {
|
||||
const onSave = jest.fn();
|
||||
const history = createMemoryHistory();
|
||||
const wrapper = ({ children }: any) => (
|
||||
<Router history={history}>{children}</Router>
|
||||
);
|
||||
|
||||
const locationState = { fromDashboard: true, dashboardId: 123 };
|
||||
const pathname = '/another-page';
|
||||
const search = '?slice_id=42&foo=bar';
|
||||
|
||||
// Simulate a blocked navigation (the hook sets up a blocker internally)
|
||||
await act(async () => {
|
||||
result.current.router.history.push(`${pathname}${search}`, locationState);
|
||||
const { result } = renderHook(
|
||||
() => useUnsavedChangesPrompt({ hasUnsavedChanges: true, onSave }),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const pushSpy = jest.spyOn(history, 'push');
|
||||
|
||||
// Simulate a blocked navigation (the hook sets up history.block internally)
|
||||
act(() => {
|
||||
history.push({ pathname, search }, locationState);
|
||||
});
|
||||
|
||||
// Modal should now be visible
|
||||
await waitFor(() => expect(result.current.prompt.showModal).toBe(true));
|
||||
expect(result.current.showModal).toBe(true);
|
||||
|
||||
// Confirm navigation
|
||||
await act(async () => {
|
||||
result.current.prompt.handleConfirmNavigation();
|
||||
act(() => {
|
||||
result.current.handleConfirmNavigation();
|
||||
});
|
||||
|
||||
// Modal should close
|
||||
expect(result.current.prompt.showModal).toBe(false);
|
||||
expect(result.current.showModal).toBe(false);
|
||||
|
||||
// Verify the blocked navigation resumed with pathname, search, and state
|
||||
await waitFor(() =>
|
||||
expect(result.current.router.state.location.pathname).toBe(pathname),
|
||||
);
|
||||
expect(result.current.router.state.location.search).toEqual({
|
||||
slice_id: '42',
|
||||
foo: 'bar',
|
||||
});
|
||||
expect(result.current.router.state.location.state).toMatchObject(
|
||||
locationState,
|
||||
);
|
||||
});
|
||||
|
||||
test('should discard the blocked navigation when modal is dismissed', async () => {
|
||||
const { result } = await setup();
|
||||
|
||||
await act(async () => {
|
||||
result.current.router.history.push('/another-page');
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.prompt.showModal).toBe(true));
|
||||
|
||||
// Dismiss the modal without confirming
|
||||
await act(async () => {
|
||||
result.current.prompt.setShowModal(false);
|
||||
});
|
||||
|
||||
expect(result.current.prompt.showModal).toBe(false);
|
||||
expect(result.current.router.state.location.pathname).toBe('/dashboard');
|
||||
// Verify correct call with pathname, search, and state preserved
|
||||
expect(pushSpy).toHaveBeenCalledWith({ pathname, search }, locationState);
|
||||
|
||||
pushSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -26,9 +26,9 @@ import {
|
||||
createStore,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { Provider } from 'react-redux';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
||||
import AlertListComponent from 'src/pages/AlertReportList';
|
||||
|
||||
jest.setTimeout(30000);
|
||||
@@ -153,11 +153,11 @@ const renderAlertList = (props: Record<string, any> = {}) => {
|
||||
const store = createStore();
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<StandaloneRouter>
|
||||
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||
<MemoryRouter>
|
||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
||||
<AlertList user={mockUser} {...props} />
|
||||
</QueryParamProvider>
|
||||
</StandaloneRouter>
|
||||
</MemoryRouter>
|
||||
</Provider>,
|
||||
);
|
||||
};
|
||||
@@ -451,11 +451,11 @@ test('read-only users do not see delete and bulk select controls', async () => {
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<StandaloneRouter>
|
||||
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||
<MemoryRouter>
|
||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
||||
<AlertList user={readOnlyUser} />
|
||||
</QueryParamProvider>
|
||||
</StandaloneRouter>
|
||||
</MemoryRouter>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
SupersetClient,
|
||||
@@ -376,13 +376,11 @@ function AlertList({
|
||||
},
|
||||
{
|
||||
Cell: ({ row: { original } }: any) => {
|
||||
const navigate = useNavigate();
|
||||
const history = useHistory();
|
||||
const handleEdit = () => handleAlertEdit(original);
|
||||
const handleDelete = () => setCurrentAlertDeleting(original);
|
||||
const handleGotoExecutionLog = () =>
|
||||
navigate({
|
||||
to: `/${original.type.toLowerCase()}/${original.id}/log`,
|
||||
});
|
||||
history.push(`/${original.type.toLowerCase()}/${original.id}/log`);
|
||||
|
||||
const allowEdit =
|
||||
original.owners.map((o: Owner) => o.id).includes(user.userId) ||
|
||||
|
||||
@@ -25,9 +25,9 @@ import {
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
||||
|
||||
import React from 'react';
|
||||
import AnnotationLayersListComponent from 'src/pages/AnnotationLayerList';
|
||||
@@ -81,11 +81,11 @@ fetchMock.get(layersRelatedEndpoint, {
|
||||
|
||||
const renderAnnotationLayersList = (props = {}) =>
|
||||
render(
|
||||
<StandaloneRouter>
|
||||
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||
<MemoryRouter>
|
||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
||||
<AnnotationLayersList user={mockUser} {...props} />
|
||||
</QueryParamProvider>
|
||||
</StandaloneRouter>,
|
||||
</MemoryRouter>,
|
||||
{
|
||||
useRedux: true,
|
||||
store,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useMemo, useState } from 'react';
|
||||
import rison from 'rison';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { Link, useRouter } from '@tanstack/react-router';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { useListViewResource } from 'src/views/CRUD/hooks';
|
||||
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
@@ -140,10 +140,16 @@ function AnnotationLayersList({
|
||||
original: { id, name },
|
||||
},
|
||||
}: any) => {
|
||||
// If no router context exists, we know not to use <Link> in render
|
||||
const hasRouter = Boolean(useRouter({ warn: false }));
|
||||
let hasHistory = true;
|
||||
|
||||
if (hasRouter) {
|
||||
try {
|
||||
useHistory();
|
||||
} catch (err) {
|
||||
// If error is thrown, we know not to use <Link> in render
|
||||
hasHistory = false;
|
||||
}
|
||||
|
||||
if (hasHistory) {
|
||||
return <Link to={`/annotationlayer/${id}/annotation`}>{name}</Link>;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, Link, useRouter } from '@tanstack/react-router';
|
||||
import { useParams, Link, useHistory } from 'react-router-dom';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { SupersetClient, getClientErrorObject } from '@superset-ui/core';
|
||||
import { css, styled } from '@apache-superset/core/theme';
|
||||
@@ -68,7 +68,7 @@ function AnnotationList({
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
}: AnnotationListProps) {
|
||||
const { annotationLayerId }: any = useParams({ strict: false });
|
||||
const { annotationLayerId }: any = useParams();
|
||||
const {
|
||||
state: {
|
||||
loading,
|
||||
@@ -247,8 +247,14 @@ function AnnotationList({
|
||||
},
|
||||
});
|
||||
|
||||
// If no router context exists, we know not to use <Link> in render
|
||||
const hasRouter = Boolean(useRouter({ warn: false }));
|
||||
let hasHistory = true;
|
||||
|
||||
try {
|
||||
useHistory();
|
||||
} catch (err) {
|
||||
// If error is thrown, we know not to use <Link> in render
|
||||
hasHistory = false;
|
||||
}
|
||||
|
||||
const emptyState = {
|
||||
title: t('No annotation yet'),
|
||||
@@ -271,7 +277,7 @@ function AnnotationList({
|
||||
<StyledHeader>
|
||||
<span>{t('Annotation Layer %s', annotationLayerName)}</span>
|
||||
<span>
|
||||
{hasRouter ? (
|
||||
{hasHistory ? (
|
||||
<Link to="/annotationlayer/list/">{t('Back to all')}</Link>
|
||||
) : (
|
||||
<Typography.Link href="/annotationlayer/list/">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
render,
|
||||
waitFor,
|
||||
@@ -261,9 +261,11 @@ describe('ChartPage', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<Link
|
||||
to="/"
|
||||
search={{ [URL_PARAMS.dashboardPageId.name]: dashboardPageId }}
|
||||
state={{ saveAction: 'overwrite' }}
|
||||
to={{
|
||||
pathname: '/',
|
||||
search: `?${URL_PARAMS.dashboardPageId.name}=${dashboardPageId}`,
|
||||
state: { saveAction: 'overwrite' },
|
||||
}}
|
||||
>
|
||||
Change route
|
||||
</Link>
|
||||
@@ -317,9 +319,7 @@ describe('ChartPage', () => {
|
||||
});
|
||||
render(
|
||||
<>
|
||||
<Link to="/" search={{ slice_id: '99' }}>
|
||||
Navigate away
|
||||
</Link>
|
||||
<Link to="/?slice_id=99">Navigate away</Link>
|
||||
<ChartPage />
|
||||
</>,
|
||||
{
|
||||
@@ -410,9 +410,7 @@ describe('ChartPage', () => {
|
||||
|
||||
render(
|
||||
<>
|
||||
<Link to="/" search={{ slice_id: '99' }}>
|
||||
Navigate
|
||||
</Link>
|
||||
<Link to="/?slice_id=99">Navigate</Link>
|
||||
<ChartPage />
|
||||
</>,
|
||||
{
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useRouter, type HistoryLocation } from '@tanstack/react-router';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import type { Location, Action } from 'history';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
getLabelsColorMap,
|
||||
@@ -135,7 +136,7 @@ export default function ExplorePage() {
|
||||
const fetchGeneration = useRef(0);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
const router = useRouter();
|
||||
const history = useHistory();
|
||||
|
||||
const loadExploreData = useCallback(
|
||||
(
|
||||
@@ -290,36 +291,29 @@ export default function ExplorePage() {
|
||||
|
||||
// Initial fetch on mount
|
||||
useEffect(() => {
|
||||
loadExploreData(router.history.location);
|
||||
loadExploreData(history.location);
|
||||
getLabelsColorMap().source = LabelsColorMapSource.Explore;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Re-fetch on navigation or post-save.
|
||||
// PUSH/BACK/FORWARD/GO: full reload (unmount + re-fetch).
|
||||
// PUSH/POP: full reload (unmount + re-fetch).
|
||||
// REPLACE with saveAction state: re-fetch without unmount (keeps chart visible).
|
||||
// Other REPLACE: ignored (URL sync from updateHistory).
|
||||
useEffect(() => {
|
||||
const unsubscribe = router.history.subscribe(
|
||||
({
|
||||
location: loc,
|
||||
action,
|
||||
}: {
|
||||
location: HistoryLocation;
|
||||
action: { type: string };
|
||||
}) => {
|
||||
const saveAction = (loc.state as Record<string, unknown>)
|
||||
?.saveAction as SaveActionType | undefined;
|
||||
if (action.type !== 'REPLACE') {
|
||||
setIsLoaded(false);
|
||||
loadExploreData(loc, saveAction);
|
||||
} else if (saveAction) {
|
||||
loadExploreData(loc, saveAction);
|
||||
}
|
||||
},
|
||||
);
|
||||
return unsubscribe;
|
||||
}, [router, loadExploreData]);
|
||||
const unlisten = history.listen((loc: Location, action: Action) => {
|
||||
const saveAction = (loc.state as Record<string, unknown>)?.saveAction as
|
||||
| SaveActionType
|
||||
| undefined;
|
||||
if (action === 'PUSH' || action === 'POP') {
|
||||
setIsLoaded(false);
|
||||
loadExploreData(loc, saveAction);
|
||||
} else if (saveAction) {
|
||||
loadExploreData(loc, saveAction);
|
||||
}
|
||||
});
|
||||
return unlisten;
|
||||
}, [history, loadExploreData]);
|
||||
|
||||
if (!isLoaded) {
|
||||
return <Loading />;
|
||||
|
||||
@@ -24,7 +24,7 @@ import { styled } from '@apache-superset/core/theme';
|
||||
import { withTheme, Theme } from '@emotion/react';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { FilterPlugins, URL_PARAMS } from 'src/constants';
|
||||
import { Link, useRouter } from '@tanstack/react-router';
|
||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import {
|
||||
AsyncSelect,
|
||||
Button,
|
||||
@@ -49,11 +49,10 @@ import {
|
||||
datasetLabelLower,
|
||||
} from 'src/features/semanticLayers/label';
|
||||
|
||||
export interface ChartCreationProps {
|
||||
export interface ChartCreationProps extends RouteComponentProps {
|
||||
user: UserWithPermissionsAndRoles;
|
||||
addSuccessToast: (arg: string) => void;
|
||||
theme: Theme;
|
||||
history: { push: (path: string) => void };
|
||||
}
|
||||
|
||||
export type ChartCreationState = {
|
||||
@@ -398,11 +397,4 @@ export class ChartCreation extends PureComponent<
|
||||
}
|
||||
}
|
||||
|
||||
const ChartCreationWithToastsAndTheme = withToasts(withTheme(ChartCreation));
|
||||
|
||||
export default function ChartCreationPage(props: {
|
||||
user: UserWithPermissionsAndRoles;
|
||||
}) {
|
||||
const { history } = useRouter();
|
||||
return <ChartCreationWithToastsAndTheme {...props} history={history} />;
|
||||
}
|
||||
export default withRouter(withToasts(withTheme(ChartCreation)));
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
||||
import { isFeatureEnabled } from '@superset-ui/core';
|
||||
import ChartList from 'src/pages/ChartList';
|
||||
import { API_ENDPOINTS, mockCharts, setupMocks } from './ChartList.testHelpers';
|
||||
@@ -116,11 +116,11 @@ const renderChartList = (
|
||||
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||
<MemoryRouter>
|
||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
||||
<ChartList user={user} {...props} />
|
||||
</QueryParamProvider>
|
||||
</StandaloneRouter>
|
||||
</MemoryRouter>
|
||||
</Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
||||
import ChartList from 'src/pages/ChartList';
|
||||
import handleResourceExport from 'src/utils/export';
|
||||
|
||||
@@ -267,15 +267,13 @@ export const renderChartList = (user: any, props = {}, storeState = {}) => {
|
||||
|
||||
const store = createMockStore(storeStateWithUser);
|
||||
|
||||
// Browser (jsdom) history, not memory history: tests assert on
|
||||
// window.location after navigation, matching the old BrowserRouter.
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<StandaloneRouter>
|
||||
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||
<BrowserRouter>
|
||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
||||
<ChartList user={user} {...props} />
|
||||
</QueryParamProvider>
|
||||
</StandaloneRouter>
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -68,8 +68,7 @@ import {
|
||||
type ListViewFilter,
|
||||
} from 'src/components';
|
||||
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
|
||||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
import { parseSearch } from 'src/router/searchParams';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { dangerouslyGetItemDoNotUse } from 'src/utils/localStorageHelpers';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||
@@ -176,7 +175,7 @@ function ChartList(props: ChartListProps) {
|
||||
user: { userId },
|
||||
} = props;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
state: {
|
||||
@@ -366,31 +365,22 @@ function ChartList(props: ChartListProps) {
|
||||
description,
|
||||
},
|
||||
},
|
||||
}: any) => {
|
||||
// url comes from the backend and may contain a query string
|
||||
// (cast: search prop types collapse to never for dynamic 'to' strings)
|
||||
const [pathname, queryString] = url.split('?');
|
||||
return (
|
||||
<FlexRowContainer>
|
||||
<Link
|
||||
to={pathname}
|
||||
search={parseSearch(queryString ?? '') as never}
|
||||
data-test={`${sliceName}-list-chart-title`}
|
||||
>
|
||||
{certifiedBy && (
|
||||
<>
|
||||
<CertifiedBadge
|
||||
certifiedBy={certifiedBy}
|
||||
details={certificationDetails}
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
{sliceName}
|
||||
</Link>
|
||||
{description && <InfoTooltip tooltip={description} />}
|
||||
</FlexRowContainer>
|
||||
);
|
||||
},
|
||||
}: any) => (
|
||||
<FlexRowContainer>
|
||||
<Link to={url} data-test={`${sliceName}-list-chart-title`}>
|
||||
{certifiedBy && (
|
||||
<>
|
||||
<CertifiedBadge
|
||||
certifiedBy={certifiedBy}
|
||||
details={certificationDetails}
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
{sliceName}
|
||||
</Link>
|
||||
{description && <InfoTooltip tooltip={description} />}
|
||||
</FlexRowContainer>
|
||||
),
|
||||
Header: t('Name'),
|
||||
accessor: 'slice_name',
|
||||
id: 'slice_name',
|
||||
@@ -867,7 +857,7 @@ function ChartList(props: ChartListProps) {
|
||||
name: t('Chart'),
|
||||
buttonStyle: 'primary',
|
||||
onClick: () => {
|
||||
navigate({ to: '/chart/add' });
|
||||
history.push('/chart/add');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ import {
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
||||
|
||||
import React from 'react';
|
||||
import CssTemplatesListComponent from 'src/pages/CssTemplateList';
|
||||
@@ -81,11 +81,11 @@ fetchMock.get(templatesRelatedEndpoint, {
|
||||
|
||||
const renderCssTemplatesList = (props = {}) =>
|
||||
render(
|
||||
<StandaloneRouter>
|
||||
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||
<MemoryRouter>
|
||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
||||
<CssTemplatesList user={mockUser} {...props} />
|
||||
</QueryParamProvider>
|
||||
</StandaloneRouter>,
|
||||
</MemoryRouter>,
|
||||
{
|
||||
useRedux: true,
|
||||
store,
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { useParams } from '@tanstack/react-router';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { DashboardPage } from 'src/dashboard/containers/DashboardPage';
|
||||
|
||||
const DashboardRoute: FC = () => {
|
||||
const { idOrSlug } = useParams({ strict: false }) as { idOrSlug: string };
|
||||
const { idOrSlug } = useParams<{ idOrSlug: string }>();
|
||||
return <DashboardPage idOrSlug={idOrSlug} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
||||
import { isFeatureEnabled } from '@superset-ui/core';
|
||||
import DashboardListComponent from 'src/pages/DashboardList';
|
||||
import {
|
||||
@@ -123,11 +123,11 @@ const renderDashboardListWithPermissions = (
|
||||
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<StandaloneRouter initialEntries={['/']}>
|
||||
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||
<MemoryRouter>
|
||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
||||
<DashboardList user={user} {...props} />
|
||||
</QueryParamProvider>
|
||||
</StandaloneRouter>
|
||||
</MemoryRouter>
|
||||
</Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user