mirror of
https://github.com/apache/superset.git
synced 2026-06-28 02:45:32 +00:00
Compare commits
1 Commits
chore/ci/s
...
tanstack-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c233d92a3 |
183
superset-frontend/package-lock.json
generated
183
superset-frontend/package-lock.json
generated
@@ -71,6 +71,7 @@
|
|||||||
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
||||||
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
|
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
|
||||||
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
||||||
|
"@tanstack/react-router": "^1.170.15",
|
||||||
"@types/d3-format": "^3.0.1",
|
"@types/d3-format": "^3.0.1",
|
||||||
"@types/d3-selection": "^3.0.11",
|
"@types/d3-selection": "^3.0.11",
|
||||||
"@types/d3-time-format": "^4.0.3",
|
"@types/d3-time-format": "^4.0.3",
|
||||||
@@ -135,7 +136,6 @@
|
|||||||
"react-redux": "^7.2.9",
|
"react-redux": "^7.2.9",
|
||||||
"react-resize-detector": "^9.1.1",
|
"react-resize-detector": "^9.1.1",
|
||||||
"react-reverse-portal": "^2.3.0",
|
"react-reverse-portal": "^2.3.0",
|
||||||
"react-router-dom": "^5.3.4",
|
|
||||||
"react-search-input": "^0.11.3",
|
"react-search-input": "^0.11.3",
|
||||||
"react-split": "^2.0.9",
|
"react-split": "^2.0.9",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
@@ -206,7 +206,6 @@
|
|||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "^18.2.0",
|
||||||
"@types/react-loadable": "^5.5.11",
|
"@types/react-loadable": "^5.5.11",
|
||||||
"@types/react-redux": "^7.1.10",
|
"@types/react-redux": "^7.1.10",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"@types/react-transition-group": "^4.4.12",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@types/redux-localstorage": "^1.0.8",
|
"@types/redux-localstorage": "^1.0.8",
|
||||||
@@ -10754,6 +10753,89 @@
|
|||||||
"@swc/counter": "^0.1.3"
|
"@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": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "9.3.4",
|
"version": "9.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
|
||||||
@@ -11407,13 +11489,6 @@
|
|||||||
"@types/unist": "^2"
|
"@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": {
|
"node_modules/@types/hoist-non-react-statics": {
|
||||||
"version": "3.3.6",
|
"version": "3.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
|
||||||
@@ -11812,29 +11887,6 @@
|
|||||||
"redux": "^4.0.0"
|
"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": {
|
"node_modules/@types/react-syntax-highlighter": {
|
||||||
"version": "15.5.13",
|
"version": "15.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
||||||
@@ -16439,6 +16491,12 @@
|
|||||||
"node": ">= 0.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": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
@@ -24131,6 +24189,15 @@
|
|||||||
"url": "https://github.com/sponsors/gjtorikian/"
|
"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": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -35981,6 +36048,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
|
||||||
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
|
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.13",
|
"@babel/runtime": "^7.12.13",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
@@ -36001,6 +36070,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
|
||||||
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
|
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.13",
|
"@babel/runtime": "^7.12.13",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
@@ -36019,6 +36090,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.1.2",
|
"@babel/runtime": "^7.1.2",
|
||||||
"loose-envify": "^1.2.0",
|
"loose-envify": "^1.2.0",
|
||||||
@@ -36033,6 +36106,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.1.2",
|
"@babel/runtime": "^7.1.2",
|
||||||
"loose-envify": "^1.2.0",
|
"loose-envify": "^1.2.0",
|
||||||
@@ -36046,13 +36121,17 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
|
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/react-router/node_modules/path-to-regexp": {
|
"node_modules/react-router/node_modules/path-to-regexp": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
|
||||||
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
|
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isarray": "0.0.1"
|
"isarray": "0.0.1"
|
||||||
}
|
}
|
||||||
@@ -36061,7 +36140,9 @@
|
|||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/react-search-input": {
|
"node_modules/react-search-input": {
|
||||||
"version": "0.11.3",
|
"version": "0.11.3",
|
||||||
@@ -37362,7 +37443,9 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
||||||
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
|
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/resolve-pkg-maps": {
|
"node_modules/resolve-pkg-maps": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -37798,6 +37881,27 @@
|
|||||||
"integrity": "sha512-y9WzzDj3BsGgKLCh0ugiinufS//YqOfao/yVJjkXA4VLuyNCfHOLU/cbulGPxs3aeCqhvROw7qPL04JSZnCo0w==",
|
"integrity": "sha512-y9WzzDj3BsGgKLCh0ugiinufS//YqOfao/yVJjkXA4VLuyNCfHOLU/cbulGPxs3aeCqhvROw7qPL04JSZnCo0w==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/serve-index": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
|
||||||
@@ -40082,13 +40186,16 @@
|
|||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tiny-warning": {
|
"node_modules/tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/tinycolor2": {
|
"node_modules/tinycolor2": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
@@ -42091,7 +42198,9 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
|
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
|
|||||||
@@ -154,6 +154,7 @@
|
|||||||
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
||||||
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
|
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
|
||||||
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
||||||
|
"@tanstack/react-router": "^1.170.15",
|
||||||
"@types/d3-format": "^3.0.1",
|
"@types/d3-format": "^3.0.1",
|
||||||
"@types/d3-selection": "^3.0.11",
|
"@types/d3-selection": "^3.0.11",
|
||||||
"@types/d3-time-format": "^4.0.3",
|
"@types/d3-time-format": "^4.0.3",
|
||||||
@@ -218,7 +219,6 @@
|
|||||||
"react-redux": "^7.2.9",
|
"react-redux": "^7.2.9",
|
||||||
"react-resize-detector": "^9.1.1",
|
"react-resize-detector": "^9.1.1",
|
||||||
"react-reverse-portal": "^2.3.0",
|
"react-reverse-portal": "^2.3.0",
|
||||||
"react-router-dom": "^5.3.4",
|
|
||||||
"react-search-input": "^0.11.3",
|
"react-search-input": "^0.11.3",
|
||||||
"react-split": "^2.0.9",
|
"react-split": "^2.0.9",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
@@ -289,7 +289,6 @@
|
|||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "^18.2.0",
|
||||||
"@types/react-loadable": "^5.5.11",
|
"@types/react-loadable": "^5.5.11",
|
||||||
"@types/react-redux": "^7.1.10",
|
"@types/react-redux": "^7.1.10",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"@types/react-transition-group": "^4.4.12",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@types/redux-localstorage": "^1.0.8",
|
"@types/redux-localstorage": "^1.0.8",
|
||||||
|
|||||||
@@ -19,18 +19,18 @@
|
|||||||
|
|
||||||
import { ThemeProvider } from '@apache-superset/core/theme';
|
import { ThemeProvider } from '@apache-superset/core/theme';
|
||||||
import querystring from 'query-string';
|
import querystring from 'query-string';
|
||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
|
||||||
|
|
||||||
export function ProviderWrapper(props: any) {
|
export function ProviderWrapper(props: any) {
|
||||||
const { children, theme } = props;
|
const { children, theme } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Router>
|
<StandaloneRouter>
|
||||||
<QueryParamProvider
|
<QueryParamProvider
|
||||||
adapter={ReactRouter5Adapter}
|
adapter={TanstackRouterAdapter}
|
||||||
options={{
|
options={{
|
||||||
searchStringToObject: querystring.parse,
|
searchStringToObject: querystring.parse,
|
||||||
objectToSearchString: (object: Record<string, any>) =>
|
objectToSearchString: (object: Record<string, any>) =>
|
||||||
@@ -39,7 +39,7 @@ export function ProviderWrapper(props: any) {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</Router>
|
</StandaloneRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ import {
|
|||||||
} from '@apache-superset/core/theme';
|
} from '@apache-superset/core/theme';
|
||||||
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
|
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
|
||||||
import { ThemeController } from 'src/theme/ThemeController';
|
import { ThemeController } from 'src/theme/ThemeController';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { DndContext } from '@dnd-kit/core';
|
import { DndContext } from '@dnd-kit/core';
|
||||||
import reducerIndex from 'spec/helpers/reducerIndex';
|
import reducerIndex from 'spec/helpers/reducerIndex';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import { configureStore, Store } from '@reduxjs/toolkit';
|
import { configureStore, Store } from '@reduxjs/toolkit';
|
||||||
import { api } from 'src/hooks/apiResources/queryApi';
|
import { api } from 'src/hooks/apiResources/queryApi';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
@@ -55,6 +55,8 @@ type Options = Omit<RenderOptions, 'queries'> & {
|
|||||||
initialState?: {};
|
initialState?: {};
|
||||||
reducers?: {};
|
reducers?: {};
|
||||||
store?: Store;
|
store?: Store;
|
||||||
|
/** Starting history entries for the test router (memory history). */
|
||||||
|
initialEntries?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const themeController = new ThemeController({ themeObject });
|
const themeController = new ThemeController({ themeObject });
|
||||||
@@ -84,6 +86,7 @@ export function createWrapper(options?: Options) {
|
|||||||
initialState,
|
initialState,
|
||||||
reducers,
|
reducers,
|
||||||
store,
|
store,
|
||||||
|
initialEntries,
|
||||||
} = options || {};
|
} = options || {};
|
||||||
|
|
||||||
return ({ children }: { children?: ReactNode }) => {
|
return ({ children }: { children?: ReactNode }) => {
|
||||||
@@ -116,14 +119,18 @@ export function createWrapper(options?: Options) {
|
|||||||
|
|
||||||
if (useQueryParams) {
|
if (useQueryParams) {
|
||||||
result = (
|
result = (
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
{result}
|
{result}
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useRouter) {
|
if (useRouter || useQueryParams || initialEntries) {
|
||||||
result = <BrowserRouter>{result}</BrowserRouter>;
|
result = (
|
||||||
|
<StandaloneRouter initialEntries={initialEntries}>
|
||||||
|
{result}
|
||||||
|
</StandaloneRouter>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Navigate } from '@tanstack/react-router';
|
||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { css, styled } from '@apache-superset/core/theme';
|
import { css, styled } from '@apache-superset/core/theme';
|
||||||
@@ -208,13 +208,7 @@ class App extends PureComponent<AppProps, AppState> {
|
|||||||
render() {
|
render() {
|
||||||
const { queries, queriesLastUpdate } = this.props;
|
const { queries, queriesLastUpdate } = this.props;
|
||||||
if (this.state.hash && this.state.hash === '#search') {
|
if (this.state.hash && this.state.hash === '#search') {
|
||||||
return (
|
return <Navigate to="/sqllab/history/" replace />;
|
||||||
<Redirect
|
|
||||||
to={{
|
|
||||||
pathname: '/sqllab/history/',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<SqlLabStyles data-test="SqlLabApp" className="App SqlLab">
|
<SqlLabStyles data-test="SqlLabApp" className="App SqlLab">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { render, waitFor } from 'spec/helpers/testing-library';
|
import { render, waitFor } from 'spec/helpers/testing-library';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { initialState } from 'src/SqlLab/fixtures';
|
import { initialState } from 'src/SqlLab/fixtures';
|
||||||
@@ -32,11 +32,11 @@ const setup = (
|
|||||||
overridesInitialState?: RootState,
|
overridesInitialState?: RootState,
|
||||||
) =>
|
) =>
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={[url]}>
|
<StandaloneRouter initialEntries={[url]}>
|
||||||
<LocationProvider>
|
<LocationProvider>
|
||||||
<PopEditorTab />
|
<PopEditorTab />
|
||||||
</LocationProvider>
|
</LocationProvider>
|
||||||
</MemoryRouter>,
|
</StandaloneRouter>,
|
||||||
{
|
{
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
initialState: overridesInitialState || initialState,
|
initialState: overridesInitialState || initialState,
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import {
|
|||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { shallowEqual, useSelector } from 'react-redux';
|
import { shallowEqual, useSelector } from 'react-redux';
|
||||||
import { useAppDispatch } from 'src/SqlLab/hooks/useAppDispatch';
|
import { useAppDispatch } from 'src/SqlLab/hooks/useAppDispatch';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -232,7 +233,7 @@ const ResultSet = ({
|
|||||||
canExportDataSqlLab: canExportData,
|
canExportDataSqlLab: canExportData,
|
||||||
canCopyClipboardSqlLab: canCopyClipboard,
|
canCopyClipboardSqlLab: canCopyClipboard,
|
||||||
} = usePermissions();
|
} = usePermissions();
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const logAction = useLogAction({ queryId, sqlEditorId: query.sqlEditorId });
|
const logAction = useLogAction({ queryId, sqlEditorId: query.sqlEditorId });
|
||||||
const { showConfirm, ConfirmModal } = useConfirmModal();
|
const { showConfirm, ConfirmModal } = useConfirmModal();
|
||||||
@@ -314,7 +315,7 @@ const ResultSet = ({
|
|||||||
if (openInNewWindow) {
|
if (openInNewWindow) {
|
||||||
window.open(url, '_blank', 'noreferrer');
|
window.open(url, '_blank', 'noreferrer');
|
||||||
} else {
|
} else {
|
||||||
history.push(url);
|
pushAppHref(router, url);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addDangerToast(t('Unable to create chart without a query id.'));
|
addDangerToast(t('Unable to create chart without a query id.'));
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
import { Alert } from '@apache-superset/core/components';
|
import { Alert } from '@apache-superset/core/components';
|
||||||
import { css, useTheme } from '@apache-superset/core/theme';
|
import { css, useTheme } from '@apache-superset/core/theme';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -78,7 +78,7 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { addDangerToast } = useToasts();
|
const { addDangerToast } = useToasts();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [url, setUrl] = useState('');
|
const [formDataKey, setFormDataKey] = useState('');
|
||||||
const dashboardPageId = useContext(DashboardPageIdContext);
|
const dashboardPageId = useContext(DashboardPageIdContext);
|
||||||
const onEditChartClick = useCallback(() => {
|
const onEditChartClick = useCallback(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -97,9 +97,7 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
|
|||||||
if (isEmbedded()) return;
|
if (isEmbedded()) return;
|
||||||
postFormData(Number(datasource_id), datasource_type, formData, 0)
|
postFormData(Number(datasource_id), datasource_type, formData, 0)
|
||||||
.then(key => {
|
.then(key => {
|
||||||
setUrl(
|
setFormDataKey(key);
|
||||||
`/explore/?form_data_key=${key}&dashboard_page_id=${dashboardPageId}`,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
addDangerToast(t('Failed to generate chart edit URL'));
|
addDangerToast(t('Failed to generate chart edit URL'));
|
||||||
@@ -111,7 +109,7 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
|
|||||||
datasource_type,
|
datasource_type,
|
||||||
formData,
|
formData,
|
||||||
]);
|
]);
|
||||||
const isEditDisabled = !url || !canExplore;
|
const isEditDisabled = !formDataKey || !canExplore;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -133,7 +131,11 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
to={url}
|
to="/explore/"
|
||||||
|
search={{
|
||||||
|
form_data_key: formDataKey,
|
||||||
|
dashboard_page_id: dashboardPageId,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('Edit chart')}
|
{t('Edit chart')}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ import DrillDetailModal from './DrillDetailModal';
|
|||||||
|
|
||||||
jest.mock('./DrillDetailPane', () => () => null);
|
jest.mock('./DrillDetailPane', () => () => null);
|
||||||
const mockHistoryPush = jest.fn();
|
const mockHistoryPush = jest.fn();
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('@tanstack/react-router', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('@tanstack/react-router'),
|
||||||
useHistory: () => ({
|
useRouter: () => ({
|
||||||
push: mockHistoryPush,
|
history: {
|
||||||
|
push: mockHistoryPush,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useContext, useMemo } from 'react';
|
import { useCallback, useContext, useMemo } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import {
|
import {
|
||||||
BinaryQueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
@@ -98,7 +99,7 @@ export default function DrillDetailModal({
|
|||||||
dataset,
|
dataset,
|
||||||
}: DrillDetailModalProps) {
|
}: DrillDetailModalProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const dashboardPageId = useContext(DashboardPageIdContext);
|
const dashboardPageId = useContext(DashboardPageIdContext);
|
||||||
const { slice_name: chartName } = useSelector(
|
const { slice_name: chartName } = useSelector(
|
||||||
(state: { sliceEntities: { slices: Record<number, Slice> } }) =>
|
(state: { sliceEntities: { slices: Record<number, Slice> } }) =>
|
||||||
@@ -114,8 +115,8 @@ export default function DrillDetailModal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const exploreChart = useCallback(() => {
|
const exploreChart = useCallback(() => {
|
||||||
history.push(exploreUrl);
|
pushAppHref(router, exploreUrl);
|
||||||
}, [exploreUrl, history]);
|
}, [exploreUrl, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -17,32 +17,49 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||||
import { PropsWithoutRef, RefAttributes } from 'react';
|
import { AnchorHTMLAttributes } from 'react';
|
||||||
import { Link, LinkProps } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
|
import { parseSearch } from 'src/router/searchParams';
|
||||||
import { isUrlExternal, parseUrl } from 'src/utils/urlUtils';
|
import { isUrlExternal, parseUrl } from 'src/utils/urlUtils';
|
||||||
|
|
||||||
export const GenericLink = <S,>({
|
export type GenericLinkProps = Omit<
|
||||||
to,
|
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
component,
|
'href'
|
||||||
|
> & {
|
||||||
|
to: string;
|
||||||
|
replace?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GenericLink = ({
|
||||||
|
to: rawTo,
|
||||||
replace,
|
replace,
|
||||||
innerRef,
|
|
||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
}: PropsWithoutRef<LinkProps<S>> & RefAttributes<HTMLAnchorElement>) => {
|
}: GenericLinkProps) => {
|
||||||
if (typeof to === 'string' && isUrlExternal(to)) {
|
// Callers may pass undefined at runtime (e.g. backend rows without a URL).
|
||||||
|
const to = typeof rawTo === 'string' ? rawTo : '';
|
||||||
|
if (to && isUrlExternal(to)) {
|
||||||
return (
|
return (
|
||||||
<a data-test="external-link" href={sanitizeUrl(parseUrl(to))} {...rest}>
|
<a data-test="external-link" href={sanitizeUrl(parseUrl(to))} {...rest}>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</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 (
|
return (
|
||||||
<Link
|
<Link
|
||||||
data-test="internal-link"
|
data-test="internal-link"
|
||||||
to={to}
|
to={pathname}
|
||||||
component={component}
|
search={searchStr ? parseSearch(searchStr) : undefined}
|
||||||
|
hash={hash}
|
||||||
replace={replace}
|
replace={replace}
|
||||||
innerRef={innerRef}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTruncation } from '@superset-ui/core';
|
import { useTruncation } from '@superset-ui/core';
|
||||||
import { styled } from '@apache-superset/core/theme';
|
import { styled } from '@apache-superset/core/theme';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
import CrossLinksTooltip from './CrossLinksTooltip';
|
import CrossLinksTooltip from './CrossLinksTooltip';
|
||||||
|
|
||||||
export type CrossLinkProps = {
|
export type CrossLinkProps = {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { styled } from '@apache-superset/core/theme';
|
import { styled } from '@apache-superset/core/theme';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
import { Tooltip } from '@superset-ui/core/components';
|
import { Tooltip } from '@superset-ui/core/components';
|
||||||
|
|
||||||
export type CrossLinksTooltipProps = {
|
export type CrossLinksTooltipProps = {
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
import { render, screen, within, waitFor } from 'spec/helpers/testing-library';
|
import { render, screen, within, waitFor } from 'spec/helpers/testing-library';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
import { ReactNode } from 'react';
|
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 factory = (overrides?: Partial<ListViewProps>) => {
|
||||||
const props = { ...mockedPropsComprehensive, ...overrides };
|
const props = { ...mockedPropsComprehensive, ...overrides };
|
||||||
return render(
|
return render(
|
||||||
<MemoryRouter>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<ListView {...props} />
|
<ListView {...props} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>,
|
</StandaloneRouter>,
|
||||||
{ store: mockStore() },
|
{ store: mockStore() },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { styled } from '@apache-superset/core/theme';
|
import { styled } from '@apache-superset/core/theme';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
import type { TagType } from 'src/types/TagType';
|
import type { TagType } from 'src/types/TagType';
|
||||||
import { Tag as AntdTag } from '@superset-ui/core/components/Tag';
|
import { Tag as AntdTag } from '@superset-ui/core/components/Tag';
|
||||||
import { Tooltip } from '@superset-ui/core/components/Tooltip';
|
import { Tooltip } from '@superset-ui/core/components/Tooltip';
|
||||||
@@ -82,7 +82,8 @@ const SupersetTag = ({
|
|||||||
{' '}
|
{' '}
|
||||||
{id ? (
|
{id ? (
|
||||||
<Link
|
<Link
|
||||||
to={`/superset/all_entities/?id=${id}`}
|
to="/superset/all_entities/"
|
||||||
|
search={{ id: String(id) }}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { DataMaskStateWithId, JsonObject } from '@superset-ui/core';
|
|||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { ThunkDispatch } from 'redux-thunk';
|
import type { ThunkDispatch } from 'redux-thunk';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import type { History } from 'history';
|
import type { RouterHistory } from '@tanstack/react-router';
|
||||||
import { chart } from 'src/components/Chart/chartReducer';
|
import { chart } from 'src/components/Chart/chartReducer';
|
||||||
import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities';
|
import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities';
|
||||||
import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters';
|
import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters';
|
||||||
@@ -92,7 +92,7 @@ interface HydrateDashboardData extends Dashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface HydrateDashboardParams {
|
interface HydrateDashboardParams {
|
||||||
history: History;
|
history: RouterHistory;
|
||||||
dashboard: HydrateDashboardData;
|
dashboard: HydrateDashboardData;
|
||||||
charts: HydrateChartData[];
|
charts: HydrateChartData[];
|
||||||
dataMask: DataMaskStateWithId;
|
dataMask: DataMaskStateWithId;
|
||||||
@@ -278,9 +278,10 @@ export const hydrateDashboard =
|
|||||||
// Removes the focused_chart parameter from the URL
|
// Removes the focused_chart parameter from the URL
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
params.delete(URL_PARAMS.dashboardFocusedChart.name);
|
params.delete(URL_PARAMS.dashboardFocusedChart.name);
|
||||||
history.replace({
|
const paramString = params.toString();
|
||||||
search: params.toString(),
|
history.replace(
|
||||||
});
|
`${history.location.pathname}${paramString ? `?${paramString}` : ''}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// find direct link component and path from root
|
// find direct link component and path from root
|
||||||
|
|||||||
@@ -32,14 +32,16 @@ import { UPDATE_COMPONENTS } from '../../actions/dashboardLayout';
|
|||||||
import { AutoRefreshStatus } from '../../types/autoRefresh';
|
import { AutoRefreshStatus } from '../../types/autoRefresh';
|
||||||
|
|
||||||
const mockHistoryReplace = jest.fn();
|
const mockHistoryReplace = jest.fn();
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('@tanstack/react-router', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('@tanstack/react-router'),
|
||||||
useHistory: () => ({
|
useRouter: () => ({
|
||||||
replace: mockHistoryReplace,
|
history: {
|
||||||
|
replace: mockHistoryReplace,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
useLocation: jest.fn(() => ({
|
useLocation: jest.fn(() => ({
|
||||||
pathname: '/dashboard',
|
pathname: '/dashboard',
|
||||||
search: '?standalone=1',
|
searchStr: 'standalone=1',
|
||||||
hash: '',
|
hash: '',
|
||||||
state: undefined,
|
state: undefined,
|
||||||
})),
|
})),
|
||||||
@@ -237,10 +239,10 @@ beforeAll(() => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
const { useLocation } = jest.requireMock('react-router-dom');
|
const { useLocation } = jest.requireMock('@tanstack/react-router');
|
||||||
useLocation.mockReturnValue({
|
useLocation.mockReturnValue({
|
||||||
pathname: '/dashboard',
|
pathname: '/dashboard',
|
||||||
search: '?standalone=1',
|
searchStr: 'standalone=1',
|
||||||
hash: '',
|
hash: '',
|
||||||
state: undefined,
|
state: undefined,
|
||||||
});
|
});
|
||||||
@@ -1051,11 +1053,11 @@ test('should sync theme ref when navigating between dashboards', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not duplicate subdirectory prefix when toggling fullscreen', async () => {
|
test('should not duplicate subdirectory prefix when toggling fullscreen', async () => {
|
||||||
const { useLocation } = jest.requireMock('react-router-dom');
|
const { useLocation } = jest.requireMock('@tanstack/react-router');
|
||||||
// Simulate React Router with basename=/pcs: useLocation returns path relative to basename
|
// Simulate React Router with basename=/pcs: useLocation returns path relative to basename
|
||||||
useLocation.mockReturnValue({
|
useLocation.mockReturnValue({
|
||||||
pathname: '/dashboard',
|
pathname: '/dashboard',
|
||||||
search: '?standalone=1',
|
searchStr: 'standalone=1',
|
||||||
hash: '',
|
hash: '',
|
||||||
state: undefined,
|
state: undefined,
|
||||||
});
|
});
|
||||||
@@ -1078,10 +1080,10 @@ test('should not duplicate subdirectory prefix when toggling fullscreen', async
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not duplicate subdirectory prefix when entering fullscreen', async () => {
|
test('should not duplicate subdirectory prefix when entering fullscreen', async () => {
|
||||||
const { useLocation } = jest.requireMock('react-router-dom');
|
const { useLocation } = jest.requireMock('@tanstack/react-router');
|
||||||
useLocation.mockReturnValue({
|
useLocation.mockReturnValue({
|
||||||
pathname: '/dashboard',
|
pathname: '/dashboard',
|
||||||
search: '',
|
searchStr: '',
|
||||||
hash: '',
|
hash: '',
|
||||||
state: undefined,
|
state: undefined,
|
||||||
});
|
});
|
||||||
@@ -1100,11 +1102,11 @@ test('should not duplicate subdirectory prefix when entering fullscreen', async
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('share URL should use browser-absolute pathname to preserve subdirectory prefix', () => {
|
test('share URL should use browser-absolute pathname to preserve subdirectory prefix', () => {
|
||||||
const { useLocation } = jest.requireMock('react-router-dom');
|
const { useLocation } = jest.requireMock('@tanstack/react-router');
|
||||||
// Router returns path without the subdirectory prefix
|
// Router returns path without the subdirectory prefix
|
||||||
useLocation.mockReturnValue({
|
useLocation.mockReturnValue({
|
||||||
pathname: '/dashboard',
|
pathname: '/dashboard',
|
||||||
search: '',
|
searchStr: '',
|
||||||
hash: '',
|
hash: '',
|
||||||
state: undefined,
|
state: undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
import type { Dispatch, ReactElement, SetStateAction } from 'react';
|
import type { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useLocation, useRouter } from '@tanstack/react-router';
|
||||||
|
import { replaceAppHref } from 'src/router/navigation';
|
||||||
import { Menu, MenuItem } from '@superset-ui/core/components/Menu';
|
import { Menu, MenuItem } from '@superset-ui/core/components/Menu';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
@@ -74,7 +75,7 @@ export const useHeaderActionsMenu = ({
|
|||||||
] => {
|
] => {
|
||||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||||
const { canExportImage } = usePermissions();
|
const { canExportImage } = usePermissions();
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const directPathToChild = useSelector(
|
const directPathToChild = useSelector(
|
||||||
(state: RootState) => state.dashboardState.directPathToChild,
|
(state: RootState) => state.dashboardState.directPathToChild,
|
||||||
@@ -102,7 +103,7 @@ export const useHeaderActionsMenu = ({
|
|||||||
case MenuKeys.ToggleFullscreen: {
|
case MenuKeys.ToggleFullscreen: {
|
||||||
const isCurrentlyStandalone =
|
const isCurrentlyStandalone =
|
||||||
Number(getUrlParam(URL_PARAMS.standalone)) === 1;
|
Number(getUrlParam(URL_PARAMS.standalone)) === 1;
|
||||||
// Use location.pathname from React Router (relative to basename) rather than
|
// Use location.pathname from the router (relative to basepath) rather than
|
||||||
// window.location.pathname to avoid duplicating the subdirectory prefix when
|
// window.location.pathname to avoid duplicating the subdirectory prefix when
|
||||||
// history.replace prepends it again.
|
// history.replace prepends it again.
|
||||||
const url = getDashboardUrl({
|
const url = getDashboardUrl({
|
||||||
@@ -111,7 +112,7 @@ export const useHeaderActionsMenu = ({
|
|||||||
hash: window.location.hash,
|
hash: window.location.hash,
|
||||||
standalone: isCurrentlyStandalone ? null : 1,
|
standalone: isCurrentlyStandalone ? null : 1,
|
||||||
});
|
});
|
||||||
history.replace(url);
|
replaceAppHref(router, url);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MenuKeys.ManageEmbedded:
|
case MenuKeys.ManageEmbedded:
|
||||||
@@ -128,7 +129,7 @@ export const useHeaderActionsMenu = ({
|
|||||||
showPropertiesModal,
|
showPropertiesModal,
|
||||||
showRefreshModal,
|
showRefreshModal,
|
||||||
manageEmbedded,
|
manageEmbedded,
|
||||||
history,
|
router,
|
||||||
location,
|
location,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,10 +16,15 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { Router } from 'react-router-dom';
|
import { createMemoryHistory } from '@tanstack/react-router';
|
||||||
import { createMemoryHistory } from 'history';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { getExtensionsRegistry, VizType } from '@superset-ui/core';
|
import { getExtensionsRegistry, VizType } from '@superset-ui/core';
|
||||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
import {
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
userEvent,
|
||||||
|
waitFor,
|
||||||
|
} from 'spec/helpers/testing-library';
|
||||||
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
|
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
|
||||||
import { useUiConfig } from 'src/components/UiConfigContext';
|
import { useUiConfig } from 'src/components/UiConfigContext';
|
||||||
import SliceHeader from '.';
|
import SliceHeader from '.';
|
||||||
@@ -283,12 +288,12 @@ test('Should render click to edit prompt and run onExploreChart on click', async
|
|||||||
initialEntries: ['/superset/dashboard/1/'],
|
initialEntries: ['/superset/dashboard/1/'],
|
||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter history={history}>
|
||||||
<SliceHeader {...props} />
|
<SliceHeader {...props} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ useRedux: true, initialState },
|
{ useRedux: true, initialState },
|
||||||
);
|
);
|
||||||
userEvent.hover(screen.getByText('Vaccine Candidates per Phase'));
|
userEvent.hover(await screen.findByText('Vaccine Candidates per Phase'));
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText('Click to edit Vaccine Candidates per Phase.'),
|
await screen.findByText('Click to edit Vaccine Candidates per Phase.'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -297,7 +302,8 @@ test('Should render click to edit prompt and run onExploreChart on click', async
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
userEvent.click(screen.getByText('Vaccine Candidates per Phase'));
|
userEvent.click(screen.getByText('Vaccine Candidates per Phase'));
|
||||||
expect(history.location.pathname).toMatch('/explore');
|
// TanStack router commits navigation asynchronously.
|
||||||
|
await waitFor(() => expect(history.location.pathname).toMatch('/explore'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Display cmd button in tooltip if running on MacOS', async () => {
|
test('Display cmd button in tooltip if running on MacOS', async () => {
|
||||||
@@ -317,18 +323,18 @@ test('Display cmd button in tooltip if running on MacOS', async () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should not render click to edit prompt and run onExploreChart on click if supersetCanExplore=false', () => {
|
test('Should not render click to edit prompt and run onExploreChart on click if supersetCanExplore=false', async () => {
|
||||||
const props = createProps({ supersetCanExplore: false });
|
const props = createProps({ supersetCanExplore: false });
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/superset/dashboard/1/'],
|
initialEntries: ['/superset/dashboard/1/'],
|
||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter history={history}>
|
||||||
<SliceHeader {...props} />
|
<SliceHeader {...props} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ useRedux: true, initialState },
|
{ useRedux: true, initialState },
|
||||||
);
|
);
|
||||||
userEvent.hover(screen.getByText('Vaccine Candidates per Phase'));
|
userEvent.hover(await screen.findByText('Vaccine Candidates per Phase'));
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText(
|
screen.queryByText(
|
||||||
'Click to edit Vaccine Candidates per Phase in a new tab',
|
'Click to edit Vaccine Candidates per Phase in a new tab',
|
||||||
@@ -339,18 +345,18 @@ test('Should not render click to edit prompt and run onExploreChart on click if
|
|||||||
expect(history.location.pathname).toMatch('/superset/dashboard');
|
expect(history.location.pathname).toMatch('/superset/dashboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should not render click to edit prompt and run onExploreChart on click if in edit mode', () => {
|
test('Should not render click to edit prompt and run onExploreChart on click if in edit mode', async () => {
|
||||||
const props = createProps({ editMode: true });
|
const props = createProps({ editMode: true });
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/superset/dashboard/1/'],
|
initialEntries: ['/superset/dashboard/1/'],
|
||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter history={history}>
|
||||||
<SliceHeader {...props} />
|
<SliceHeader {...props} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ useRedux: true, initialState },
|
{ useRedux: true, initialState },
|
||||||
);
|
);
|
||||||
userEvent.hover(screen.getByText('Vaccine Candidates per Phase'));
|
userEvent.hover(await screen.findByText('Vaccine Candidates per Phase'));
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText(
|
screen.queryByText(
|
||||||
'Click to edit Vaccine Candidates per Phase in a new tab',
|
'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 { getSliceHeaderTooltip } from 'src/dashboard/util/getSliceHeaderTooltip';
|
||||||
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
|
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
|
||||||
import RowCountLabel from 'src/components/RowCountLabel';
|
import RowCountLabel from 'src/components/RowCountLabel';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
|
|
||||||
const extensionsRegistry = getExtensionsRegistry();
|
const extensionsRegistry = getExtensionsRegistry();
|
||||||
|
|
||||||
@@ -245,7 +245,11 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
|
|||||||
|
|
||||||
const renderExploreLink = (title: string) => (
|
const renderExploreLink = (title: string) => (
|
||||||
<Link
|
<Link
|
||||||
to={exploreUrl}
|
to="/explore/"
|
||||||
|
search={{
|
||||||
|
dashboard_page_id: dashboardPageId,
|
||||||
|
slice_id: String(slice.slice_id),
|
||||||
|
}}
|
||||||
css={(theme: SupersetTheme) => css`
|
css={(theme: SupersetTheme) => css`
|
||||||
color: ${theme.colorText};
|
color: ${theme.colorText};
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { ReactChild, RefObject, useCallback } from 'react';
|
import { ReactChild, RefObject, useCallback } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { css, useTheme } from '@apache-superset/core/theme';
|
import { css, useTheme } from '@apache-superset/core/theme';
|
||||||
import { Button, ModalTrigger } from '@superset-ui/core/components';
|
import { Button, ModalTrigger } from '@superset-ui/core/components';
|
||||||
@@ -37,8 +38,9 @@ export const ViewResultsModalTrigger = ({
|
|||||||
modalBody: ReactChild;
|
modalBody: ReactChild;
|
||||||
modalRef?: RefObject<any>;
|
modalRef?: RefObject<any>;
|
||||||
}) => {
|
}) => {
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const exploreChart = () => history.push(exploreUrl);
|
// exploreUrl carries a query string; raw history push preserves it.
|
||||||
|
const exploreChart = () => pushAppHref(router, exploreUrl);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const handleCloseModal = useCallback(() => {
|
const handleCloseModal = useCallback(() => {
|
||||||
modalRef?.current?.close();
|
modalRef?.current?.close();
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import {
|
|||||||
RefObject,
|
RefObject,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { RouteComponentProps, useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import { extendedDayjs } from '@superset-ui/core/utils/dates';
|
import { extendedDayjs } from '@superset-ui/core/utils/dates';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import {
|
import {
|
||||||
@@ -144,8 +145,7 @@ export interface SliceHeaderControlsProps {
|
|||||||
|
|
||||||
crossFiltersEnabled?: boolean;
|
crossFiltersEnabled?: boolean;
|
||||||
}
|
}
|
||||||
type SliceHeaderControlsPropsWithRouter = SliceHeaderControlsProps &
|
type SliceHeaderControlsPropsWithRouter = SliceHeaderControlsProps;
|
||||||
RouteComponentProps;
|
|
||||||
|
|
||||||
const dropdownIconsStyles = css`
|
const dropdownIconsStyles = css`
|
||||||
&&.anticon > .anticon:first-of-type {
|
&&.anticon > .anticon:first-of-type {
|
||||||
@@ -169,7 +169,7 @@ const SliceHeaderControls = (
|
|||||||
const [openScopingModal, scopingModal] = useCrossFiltersScopingModal(
|
const [openScopingModal, scopingModal] = useCrossFiltersScopingModal(
|
||||||
props.slice.slice_id,
|
props.slice.slice_id,
|
||||||
);
|
);
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
|
|
||||||
const queryMenuRef: RefObject<any> = useRef(null);
|
const queryMenuRef: RefObject<any> = useRef(null);
|
||||||
const resultsMenuRef: RefObject<any> = useRef(null);
|
const resultsMenuRef: RefObject<any> = useRef(null);
|
||||||
@@ -265,7 +265,8 @@ const SliceHeaderControls = (
|
|||||||
domEvent.preventDefault();
|
domEvent.preventDefault();
|
||||||
window.open(props.exploreUrl, '_blank');
|
window.open(props.exploreUrl, '_blank');
|
||||||
} else {
|
} else {
|
||||||
history.push(props.exploreUrl);
|
// exploreUrl carries a query string; raw history push preserves it.
|
||||||
|
pushAppHref(router, props.exploreUrl);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MenuKeys.ExportCsv:
|
case MenuKeys.ExportCsv:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { FilterBarOrientation, RootState } from 'src/dashboard/types';
|
|||||||
import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
|
import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
|
||||||
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
|
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useLocation, useRouter } from '@tanstack/react-router';
|
||||||
import { removeDataMask, updateDataMask } from 'src/dataMask/actions';
|
import { removeDataMask, updateDataMask } from 'src/dataMask/actions';
|
||||||
import {
|
import {
|
||||||
getRisonFilterParam,
|
getRisonFilterParam,
|
||||||
@@ -121,8 +121,8 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
|
|||||||
state => state.dataMask,
|
state => state.dataMask,
|
||||||
);
|
);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const location = useLocation();
|
const searchStr = useLocation({ select: location => location.searchStr });
|
||||||
const chartIds = useChartIds();
|
const chartIds = useChartIds();
|
||||||
const chartLayoutItems = useChartLayoutItems();
|
const chartLayoutItems = useChartLayoutItems();
|
||||||
const verboseMaps = useChartsVerboseMaps();
|
const verboseMaps = useChartsVerboseMaps();
|
||||||
@@ -146,7 +146,7 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
|
|||||||
// programmatic history.replace).
|
// programmatic history.replace).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveUrlFilters(getUrlFilterIndicators());
|
setActiveUrlFilters(getUrlFilterIndicators());
|
||||||
}, [location.search]);
|
}, [searchStr]);
|
||||||
|
|
||||||
const handleRemoveUrlFilter = useCallback(
|
const handleRemoveUrlFilter = useCallback(
|
||||||
(filterToRemove: UrlFilterIndicator) => {
|
(filterToRemove: UrlFilterIndicator) => {
|
||||||
@@ -158,7 +158,7 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
|
|||||||
const remaining = currentFilters.filter(
|
const remaining = currentFilters.filter(
|
||||||
f => getUrlFilterIdentity(f) !== removeId,
|
f => getUrlFilterIdentity(f) !== removeId,
|
||||||
);
|
);
|
||||||
updateUrlWithUnmatchedFilters(remaining, history);
|
updateUrlWithUnmatchedFilters(remaining, router.history);
|
||||||
setActiveUrlFilters(prev =>
|
setActiveUrlFilters(prev =>
|
||||||
prev.filter(f => getUrlFilterIdentity(f.filter) !== removeId),
|
prev.filter(f => getUrlFilterIdentity(f.filter) !== removeId),
|
||||||
);
|
);
|
||||||
@@ -175,7 +175,7 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, history],
|
[dispatch, router],
|
||||||
);
|
);
|
||||||
|
|
||||||
const urlFiltersComponent = useMemo(() => {
|
const urlFiltersComponent = useMemo(() => {
|
||||||
|
|||||||
@@ -24,9 +24,15 @@
|
|||||||
* - the chip list must react to URL changes (back/forward navigation or
|
* - the chip list must react to URL changes (back/forward navigation or
|
||||||
* a programmatic history.replace), not snapshot the URL at mount.
|
* a programmatic history.replace), not snapshot the URL at mount.
|
||||||
*/
|
*/
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from '@tanstack/react-router';
|
||||||
import { Router } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { act, render, screen, userEvent } from 'spec/helpers/testing-library';
|
import {
|
||||||
|
act,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
userEvent,
|
||||||
|
waitFor,
|
||||||
|
} from 'spec/helpers/testing-library';
|
||||||
import { REMOVE_DATA_MASK, UPDATE_DATA_MASK } from 'src/dataMask/actions';
|
import { REMOVE_DATA_MASK, UPDATE_DATA_MASK } from 'src/dataMask/actions';
|
||||||
import { RISON_UNMATCHED_DATAMASK_ID } from 'src/dashboard/util/risonFilters';
|
import { RISON_UNMATCHED_DATAMASK_ID } from 'src/dashboard/util/risonFilters';
|
||||||
import UrlFiltersVertical from './Vertical';
|
import UrlFiltersVertical from './Vertical';
|
||||||
@@ -39,7 +45,7 @@ jest.mock('react-redux', () => ({
|
|||||||
|
|
||||||
const seedUrl = (search: string) => {
|
const seedUrl = (search: string) => {
|
||||||
// jsdom doesn't navigate, so set both window.location (read by
|
// jsdom doesn't navigate, so set both window.location (read by
|
||||||
// getRisonFilterParam) and react-router's in-memory history.
|
// getRisonFilterParam) and the router's in-memory history.
|
||||||
window.history.replaceState({}, '', `/superset/dashboard/1/${search}`);
|
window.history.replaceState({}, '', `/superset/dashboard/1/${search}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,9 +55,9 @@ const renderAt = (search: string) => {
|
|||||||
initialEntries: [`/superset/dashboard/1/${search}`],
|
initialEntries: [`/superset/dashboard/1/${search}`],
|
||||||
});
|
});
|
||||||
const utils = render(
|
const utils = render(
|
||||||
<Router history={history}>
|
<StandaloneRouter history={history}>
|
||||||
<UrlFiltersVertical />
|
<UrlFiltersVertical />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
return { ...utils, history };
|
return { ...utils, history };
|
||||||
@@ -120,7 +126,7 @@ test('removing the last chip dispatches removeDataMask, not an empty update', as
|
|||||||
expect(updateCalls).toHaveLength(0);
|
expect(updateCalls).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('chip list re-renders when the URL changes (popstate/programmatic nav)', () => {
|
test('chip list re-renders when the URL changes (popstate/programmatic nav)', async () => {
|
||||||
const { history } = renderAt('?f=(region:EMEA)');
|
const { history } = renderAt('?f=(region:EMEA)');
|
||||||
|
|
||||||
expect(screen.getByText('region')).toBeInTheDocument();
|
expect(screen.getByText('region')).toBeInTheDocument();
|
||||||
@@ -133,7 +139,8 @@ test('chip list re-renders when the URL changes (popstate/programmatic nav)', ()
|
|||||||
history.replace('/superset/dashboard/1/?f=(priority:high)');
|
history.replace('/superset/dashboard/1/?f=(priority:high)');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText('priority')).toBeInTheDocument();
|
// The router commits location updates asynchronously.
|
||||||
|
await waitFor(() => expect(screen.getByText('priority')).toBeInTheDocument());
|
||||||
expect(screen.getByText('high')).toBeInTheDocument();
|
expect(screen.getByText('high')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('region')).not.toBeInTheDocument();
|
expect(screen.queryByText('region')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useLocation, useRouter } from '@tanstack/react-router';
|
||||||
import { QueryObjectFilterClause } from '@superset-ui/core';
|
import { QueryObjectFilterClause } from '@superset-ui/core';
|
||||||
import { removeDataMask, updateDataMask } from 'src/dataMask/actions';
|
import { removeDataMask, updateDataMask } from 'src/dataMask/actions';
|
||||||
import {
|
import {
|
||||||
@@ -38,8 +38,8 @@ import UrlFiltersVerticalCollapse from './VerticalCollapse';
|
|||||||
|
|
||||||
const UrlFiltersVertical = () => {
|
const UrlFiltersVertical = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const location = useLocation();
|
const searchStr = useLocation({ select: location => location.searchStr });
|
||||||
const [urlFilters, setUrlFilters] = useState<UrlFilterIndicator[]>(() =>
|
const [urlFilters, setUrlFilters] = useState<UrlFilterIndicator[]>(() =>
|
||||||
getUrlFilterIndicators(),
|
getUrlFilterIndicators(),
|
||||||
);
|
);
|
||||||
@@ -48,7 +48,7 @@ const UrlFiltersVertical = () => {
|
|||||||
// programmatic history.replace).
|
// programmatic history.replace).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUrlFilters(getUrlFilterIndicators());
|
setUrlFilters(getUrlFilterIndicators());
|
||||||
}, [location.search]);
|
}, [searchStr]);
|
||||||
|
|
||||||
const handleRemoveFilter = useCallback(
|
const handleRemoveFilter = useCallback(
|
||||||
(filterToRemove: UrlFilterIndicator) => {
|
(filterToRemove: UrlFilterIndicator) => {
|
||||||
@@ -61,7 +61,7 @@ const UrlFiltersVertical = () => {
|
|||||||
f => getUrlFilterIdentity(f) !== removeId,
|
f => getUrlFilterIdentity(f) !== removeId,
|
||||||
);
|
);
|
||||||
|
|
||||||
updateUrlWithUnmatchedFilters(remaining, history);
|
updateUrlWithUnmatchedFilters(remaining, router.history);
|
||||||
setUrlFilters(prev =>
|
setUrlFilters(prev =>
|
||||||
prev.filter(f => getUrlFilterIdentity(f.filter) !== removeId),
|
prev.filter(f => getUrlFilterIdentity(f.filter) !== removeId),
|
||||||
);
|
);
|
||||||
@@ -78,7 +78,7 @@ const UrlFiltersVertical = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, history],
|
[dispatch, router],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!urlFilters.length) {
|
if (!urlFilters.length) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import {
|
|||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { styled } from '@apache-superset/core/theme';
|
import { styled } from '@apache-superset/core/theme';
|
||||||
import { Constants } from '@superset-ui/core/components';
|
import { Constants } from '@superset-ui/core/components';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter, type RouterHistory } from '@tanstack/react-router';
|
||||||
import { updateDataMask, removeDataMask } from 'src/dataMask/actions';
|
import { updateDataMask, removeDataMask } from 'src/dataMask/actions';
|
||||||
import {
|
import {
|
||||||
saveChartCustomization,
|
saveChartCustomization,
|
||||||
@@ -55,7 +55,6 @@ import { useImmer } from 'use-immer';
|
|||||||
import { isEmpty, isEqual, debounce } from 'lodash';
|
import { isEmpty, isEqual, debounce } from 'lodash';
|
||||||
import { getInitialDataMask } from 'src/dataMask/reducer';
|
import { getInitialDataMask } from 'src/dataMask/reducer';
|
||||||
import { URL_PARAMS } from 'src/constants';
|
import { URL_PARAMS } from 'src/constants';
|
||||||
import { applicationRoot } from 'src/utils/getBootstrapData';
|
|
||||||
import { getUrlParam } from 'src/utils/urlUtils';
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
import { useTabId } from 'src/hooks/useTabId';
|
import { useTabId } from 'src/hooks/useTabId';
|
||||||
import { logEvent } from 'src/logger/actions';
|
import { logEvent } from 'src/logger/actions';
|
||||||
@@ -96,7 +95,7 @@ const EMPTY_DATA_MASK_RECORD: Record<string, DataMask> = {};
|
|||||||
|
|
||||||
const publishDataMask = debounce(
|
const publishDataMask = debounce(
|
||||||
async (
|
async (
|
||||||
history,
|
history: RouterHistory,
|
||||||
dashboardId,
|
dashboardId,
|
||||||
updateKey,
|
updateKey,
|
||||||
dataMaskSelected: DataMaskStateWithId,
|
dataMaskSelected: DataMaskStateWithId,
|
||||||
@@ -145,15 +144,10 @@ const publishDataMask = debounce(
|
|||||||
// replace params only when current page is /superset/dashboard
|
// replace params only when current page is /superset/dashboard
|
||||||
// this prevents a race condition between updating filters and navigating to Explore
|
// this prevents a race condition between updating filters and navigating to Explore
|
||||||
if (window.location.pathname.includes('/superset/dashboard')) {
|
if (window.location.pathname.includes('/superset/dashboard')) {
|
||||||
// The history API is part of React router and understands that a basename may exist.
|
// The router's history is the raw browser history (no basepath
|
||||||
// Internally it treats all paths as if they are relative to the root and appends
|
// handling), so the full window pathname — application root
|
||||||
// it when necessary. We strip any prefix so that history.replace adds it back and doesn't
|
// included — is replaced verbatim.
|
||||||
// double it up.
|
const replacementPathname = window.location.pathname;
|
||||||
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
|
// Manually reconstruct the search string to preserve Rison filter encoding
|
||||||
let searchString = newParams.toString();
|
let searchString = newParams.toString();
|
||||||
if (rawRisonFilterValue) {
|
if (rawRisonFilterValue) {
|
||||||
@@ -161,10 +155,9 @@ const publishDataMask = debounce(
|
|||||||
searchString = `${searchString}${separator}f=${rawRisonFilterValue}`;
|
searchString = `${searchString}${separator}f=${rawRisonFilterValue}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
history.replace({
|
history.replace(
|
||||||
pathname: replacementPathname,
|
`${replacementPathname}${searchString ? `?${searchString}` : ''}`,
|
||||||
search: searchString,
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Constants.SLOW_DEBOUNCE,
|
Constants.SLOW_DEBOUNCE,
|
||||||
@@ -175,7 +168,7 @@ const FilterBar: FC<FiltersBarProps> = ({
|
|||||||
verticalConfig,
|
verticalConfig,
|
||||||
hidden = false,
|
hidden = false,
|
||||||
}) => {
|
}) => {
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const dataMaskApplied: DataMaskStateWithId = useAllAppliedDataMask();
|
const dataMaskApplied: DataMaskStateWithId = useAllAppliedDataMask();
|
||||||
|
|
||||||
const [dataMaskSelected, setDataMaskSelected] =
|
const [dataMaskSelected, setDataMaskSelected] =
|
||||||
@@ -406,10 +399,16 @@ const FilterBar: FC<FiltersBarProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// embedded users can't persist filter combinations
|
// embedded users can't persist filter combinations
|
||||||
if (user?.userId) {
|
if (user?.userId) {
|
||||||
publishDataMask(history, dashboardId, updateKey, dataMaskApplied, tabId);
|
publishDataMask(
|
||||||
|
router.history,
|
||||||
|
dashboardId,
|
||||||
|
updateKey,
|
||||||
|
dataMaskApplied,
|
||||||
|
tabId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [dashboardId, dataMaskAppliedText, history, updateKey, tabId]);
|
}, [dashboardId, dataMaskAppliedText, router, updateKey, tabId]);
|
||||||
|
|
||||||
const pendingChartCustomizations = useSelector<
|
const pendingChartCustomizations = useSelector<
|
||||||
RootState,
|
RootState,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
import { createContext, lazy, FC, useEffect, useMemo, useRef } from 'react';
|
import { createContext, lazy, FC, useEffect, useMemo, useRef } from 'react';
|
||||||
import { Global } from '@emotion/react';
|
import { Global } from '@emotion/react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { useTheme } from '@apache-superset/core/theme';
|
import { useTheme } from '@apache-superset/core/theme';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -128,7 +128,7 @@ const selectActiveFilters = createSelector(
|
|||||||
export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const dashboardPageId = useMemo(() => nanoid(), []);
|
const dashboardPageId = useMemo(() => nanoid(), []);
|
||||||
const hasDashboardInfoInitiated = useSelector<RootState, boolean>(
|
const hasDashboardInfoInitiated = useSelector<RootState, boolean>(
|
||||||
({ dashboardInfo }) =>
|
({ dashboardInfo }) =>
|
||||||
@@ -267,14 +267,14 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
|||||||
|
|
||||||
// Rewrite the URL to drop matched filters in a single step, keeping
|
// Rewrite the URL to drop matched filters in a single step, keeping
|
||||||
// only unmatched ones (and prettifying their encoding). Going
|
// only unmatched ones (and prettifying their encoding). Going
|
||||||
// through react-router's history keeps `history.location.search` in
|
// through the router's history keeps its location.search in
|
||||||
// sync so `publishDataMask` doesn't re-emit the original `f=`.
|
// sync so `publishDataMask` doesn't re-emit the original `f=`.
|
||||||
const matchedCount =
|
const matchedCount =
|
||||||
risonFilters.length - injectionResult.unmatchedFilters.length;
|
risonFilters.length - injectionResult.unmatchedFilters.length;
|
||||||
if (matchedCount > 0) {
|
if (matchedCount > 0) {
|
||||||
updateUrlWithUnmatchedFilters(
|
updateUrlWithUnmatchedFilters(
|
||||||
injectionResult.unmatchedFilters,
|
injectionResult.unmatchedFilters,
|
||||||
history,
|
router.history,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (injectionResult.unmatchedFilters.length > 0) {
|
if (injectionResult.unmatchedFilters.length > 0) {
|
||||||
@@ -289,7 +289,7 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
|||||||
}
|
}
|
||||||
dispatch(
|
dispatch(
|
||||||
hydrateDashboard({
|
hydrateDashboard({
|
||||||
history,
|
history: router.history,
|
||||||
dashboard: dashboard!,
|
dashboard: dashboard!,
|
||||||
charts: charts!,
|
charts: charts!,
|
||||||
activeTabs: activeTabs ?? null,
|
activeTabs: activeTabs ?? null,
|
||||||
|
|||||||
@@ -353,10 +353,10 @@ test('updateUrlWithUnmatchedFilters goes through history when supplied', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(replace).toHaveBeenCalledTimes(1);
|
expect(replace).toHaveBeenCalledTimes(1);
|
||||||
const call = replace.mock.calls[0][0];
|
const href = replace.mock.calls[0][0];
|
||||||
expect(call.pathname).toBe('/superset/dashboard/1/');
|
expect(href).toMatch(/^\/superset\/dashboard\/1\/\?/);
|
||||||
expect(call.search).toContain('f=');
|
expect(href).toContain('f=');
|
||||||
expect(call.search).toContain('region');
|
expect(href).toContain('region');
|
||||||
|
|
||||||
// Restore.
|
// Restore.
|
||||||
window.history.replaceState({}, '', originalLocation);
|
window.history.replaceState({}, '', originalLocation);
|
||||||
@@ -370,7 +370,7 @@ test('updateUrlWithUnmatchedFilters drops f= when no unmatched remain', () => {
|
|||||||
updateUrlWithUnmatchedFilters([], { replace });
|
updateUrlWithUnmatchedFilters([], { replace });
|
||||||
|
|
||||||
expect(replace).toHaveBeenCalledTimes(1);
|
expect(replace).toHaveBeenCalledTimes(1);
|
||||||
expect(replace.mock.calls[0][0].search).toBe('');
|
expect(replace.mock.calls[0][0]).toBe('/superset/dashboard/1/');
|
||||||
|
|
||||||
window.history.replaceState({}, '', originalLocation);
|
window.history.replaceState({}, '', originalLocation);
|
||||||
});
|
});
|
||||||
@@ -382,16 +382,20 @@ test('updateUrlWithUnmatchedFilters cleanup is observable by history readers', (
|
|||||||
// history.location.search stale, causing publishDataMask to re-append
|
// history.location.search stale, causing publishDataMask to re-append
|
||||||
// the original f= on the next interaction.
|
// the original f= on the next interaction.
|
||||||
//
|
//
|
||||||
// Stand in for react-router's history with a fake whose `.location`
|
// Stand in for the router's history with a fake whose `.location`
|
||||||
// updates synchronously when .replace is called — same contract as
|
// updates synchronously when .replace is called — same contract as
|
||||||
// react-router-dom's history.replace.
|
// the router history's replace.
|
||||||
const fakeHistory = {
|
const fakeHistory = {
|
||||||
location: {
|
location: {
|
||||||
pathname: '/superset/dashboard/1/',
|
pathname: '/superset/dashboard/1/',
|
||||||
search: '?f=(country:USA)',
|
search: '?f=(country:USA)',
|
||||||
},
|
},
|
||||||
replace(next: { pathname: string; search: string }) {
|
replace(href: string) {
|
||||||
this.location = next;
|
const [pathname, search = ''] = href.split('?');
|
||||||
|
this.location = {
|
||||||
|
pathname,
|
||||||
|
search: search ? `?${search}` : '',
|
||||||
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const originalLocation = window.location.href;
|
const originalLocation = window.location.href;
|
||||||
|
|||||||
@@ -318,15 +318,15 @@ export function risonFiltersToString(filters: RisonFilter[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ReplaceHistory {
|
interface ReplaceHistory {
|
||||||
replace(location: { pathname: string; search: string }): void;
|
replace(href: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the URL to remove successfully matched filters, keeping only unmatched ones.
|
* Update the URL to remove successfully matched filters, keeping only unmatched ones.
|
||||||
* When a react-router history is supplied, the update goes through it so that
|
* When a router history is supplied (e.g. `useRouter().history`), the update
|
||||||
* components reading from `history.location` (e.g. `publishDataMask` in the
|
* goes through it so that components reading the router location (e.g.
|
||||||
* filter bar) see the new search string. Otherwise falls back to a raw
|
* `publishDataMask` in the filter bar) see the new search string. Otherwise
|
||||||
* `window.history.replaceState`.
|
* falls back to a raw `window.history.replaceState`.
|
||||||
*/
|
*/
|
||||||
export function updateUrlWithUnmatchedFilters(
|
export function updateUrlWithUnmatchedFilters(
|
||||||
unmatchedFilters: RisonFilter[],
|
unmatchedFilters: RisonFilter[],
|
||||||
@@ -358,10 +358,7 @@ export function updateUrlWithUnmatchedFilters(
|
|||||||
currentUrl.toString(),
|
currentUrl.toString(),
|
||||||
);
|
);
|
||||||
if (history) {
|
if (history) {
|
||||||
history.replace({
|
history.replace(`${currentUrl.pathname}${currentUrl.search}`);
|
||||||
pathname: currentUrl.pathname,
|
|
||||||
search: currentUrl.search,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to update URL with unmatched filters:', error);
|
console.warn('Failed to update URL with unmatched filters:', error);
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ import 'src/public-path';
|
|||||||
|
|
||||||
import { lazy, Suspense, useEffect } from 'react';
|
import { lazy, Suspense, useEffect } from 'react';
|
||||||
import { createRoot, type Root } from 'react-dom/client';
|
import { createRoot, type Root } from 'react-dom/client';
|
||||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
import {
|
||||||
|
createRootRoute,
|
||||||
|
createRoute,
|
||||||
|
createRouter,
|
||||||
|
RouterProvider,
|
||||||
|
} from '@tanstack/react-router';
|
||||||
|
import { parseSearch, stringifySearch } from 'src/router/searchParams';
|
||||||
import { Global } from '@emotion/react';
|
import { Global } from '@emotion/react';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { makeApi } from '@superset-ui/core';
|
import { makeApi } from '@superset-ui/core';
|
||||||
@@ -116,13 +122,29 @@ const EmbeddedRoute = () => (
|
|||||||
</EmbeddedContextProviders>
|
</EmbeddedContextProviders>
|
||||||
);
|
);
|
||||||
|
|
||||||
const EmbeddedApp = () => (
|
const embeddedRootRoute = createRootRoute();
|
||||||
<Router basename={applicationRoot()}>
|
const embeddedRouter = createRouter({
|
||||||
{/* todo (embedded) remove this line after uuids are deployed */}
|
routeTree: embeddedRootRoute.addChildren([
|
||||||
<Route path="/dashboard/:idOrSlug/embedded/" component={EmbeddedRoute} />
|
// todo (embedded) remove this route after uuids are deployed
|
||||||
<Route path="/embedded/:uuid/" component={EmbeddedRoute} />
|
createRoute({
|
||||||
</Router>
|
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 appMountPoint = document.getElementById('app')!;
|
const appMountPoint = document.getElementById('app')!;
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter, type RouterHistory } from '@tanstack/react-router';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { QueryFormData, JsonObject } from '@superset-ui/core';
|
import { QueryFormData, JsonObject } from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
@@ -60,7 +60,7 @@ interface ExploreActions {
|
|||||||
saveFaveStar: (sliceId: number, isStarred: boolean) => void;
|
saveFaveStar: (sliceId: number, isStarred: boolean) => void;
|
||||||
redirectSQLLab: (
|
redirectSQLLab: (
|
||||||
formData: QueryFormData,
|
formData: QueryFormData,
|
||||||
history?: ReturnType<typeof useHistory> | false,
|
history?: RouterHistory | false,
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,14 +187,14 @@ const ExploreChartHeader: FC<ExploreChartHeaderProps> = ({
|
|||||||
setCurrentReportDeleting(null);
|
setCurrentReportDeleting(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const { redirectSQLLab } = actions;
|
const { redirectSQLLab } = actions;
|
||||||
|
|
||||||
const redirectToSQLLab = useCallback(
|
const redirectToSQLLab = useCallback(
|
||||||
(redirectFormData: QueryFormData, openNewWindow = false) => {
|
(redirectFormData: QueryFormData, openNewWindow = false) => {
|
||||||
redirectSQLLab(redirectFormData, !openNewWindow && history);
|
redirectSQLLab(redirectFormData, !openNewWindow && router.history);
|
||||||
},
|
},
|
||||||
[redirectSQLLab, history],
|
[redirectSQLLab, router],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [menu, isDropdownVisible, setIsDropdownVisible, streamingExportState] =
|
const [menu, isDropdownVisible, setIsDropdownVisible, streamingExportState] =
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import {
|
|||||||
VizType,
|
VizType,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { QUERY_MODE_REQUISITES } from 'src/explore/constants';
|
import { QUERY_MODE_REQUISITES } from 'src/explore/constants';
|
||||||
import { Router, Route } from 'react-router-dom';
|
import {
|
||||||
import { createMemoryHistory } from 'history';
|
createMemoryHistory,
|
||||||
|
type RouterHistory,
|
||||||
|
} from '@tanstack/react-router';
|
||||||
import {
|
import {
|
||||||
render,
|
render,
|
||||||
screen,
|
screen,
|
||||||
@@ -40,6 +42,17 @@ import reducerIndex from 'spec/helpers/reducerIndex';
|
|||||||
import * as exploreActions from 'src/explore/actions/exploreActions';
|
import * as exploreActions from 'src/explore/actions/exploreActions';
|
||||||
import ExploreViewContainer from '.';
|
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', () => ({
|
jest.doMock('@superset-ui/core', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
...jest.requireActual('@superset-ui/core'),
|
...jest.requireActual('@superset-ui/core'),
|
||||||
@@ -136,7 +149,7 @@ const renderWithRouter = ({
|
|||||||
overridePathname?: string;
|
overridePathname?: string;
|
||||||
initialState?: object;
|
initialState?: object;
|
||||||
store?: Store;
|
store?: Store;
|
||||||
history?: ReturnType<typeof createMemoryHistory>;
|
history?: RouterHistory;
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const path = overridePathname ?? defaultPath;
|
const path = overridePathname ?? defaultPath;
|
||||||
jest.spyOn(window, 'location', 'get').mockReturnValue({
|
jest.spyOn(window, 'location', 'get').mockReturnValue({
|
||||||
@@ -146,14 +159,15 @@ const renderWithRouter = ({
|
|||||||
const history =
|
const history =
|
||||||
existingHistory ??
|
existingHistory ??
|
||||||
createMemoryHistory({ initialEntries: [`${path}${search}`] });
|
createMemoryHistory({ initialEntries: [`${path}${search}`] });
|
||||||
const result = render(
|
mockRouterHistory = history;
|
||||||
<Router history={history}>
|
const result = render(<ExploreViewContainer />, {
|
||||||
<Route path={path}>
|
useRedux: true,
|
||||||
<ExploreViewContainer />
|
useDnd: true,
|
||||||
</Route>
|
initialState,
|
||||||
</Router>,
|
store,
|
||||||
{ useRedux: true, useDnd: true, initialState, store },
|
useRouter: true,
|
||||||
);
|
initialEntries: [`${path}${search}`],
|
||||||
|
});
|
||||||
return { ...result, history };
|
return { ...result, history };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import { t } from '@apache-superset/core/translation';
|
|||||||
import { logging } from '@apache-superset/core/utils';
|
import { logging } from '@apache-superset/core/utils';
|
||||||
import { debounce, isEqual, isObjectLike, omit, pick } from 'lodash';
|
import { debounce, isEqual, isObjectLike, omit, pick } from 'lodash';
|
||||||
import { Resizable } from 're-resizable';
|
import { Resizable } from 're-resizable';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
import { Tooltip } from '@superset-ui/core/components';
|
import { Tooltip } from '@superset-ui/core/components';
|
||||||
import { usePluginContext } from 'src/components';
|
import { usePluginContext } from 'src/components';
|
||||||
import { Global } from '@emotion/react';
|
import { Global } from '@emotion/react';
|
||||||
@@ -387,7 +387,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
|
|||||||
getSidebarWidths(LocalStorageKeys.DatasourceWidth),
|
getSidebarWidths(LocalStorageKeys.DatasourceWidth),
|
||||||
);
|
);
|
||||||
const tabId = useTabId();
|
const tabId = useTabId();
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@@ -477,7 +477,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
|
|||||||
props.force,
|
props.force,
|
||||||
title,
|
title,
|
||||||
tabId,
|
tabId,
|
||||||
history,
|
router.history,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -488,7 +488,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
|
|||||||
props.standalone,
|
props.standalone,
|
||||||
props.force,
|
props.force,
|
||||||
tabId,
|
tabId,
|
||||||
history,
|
router,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
/* eslint camelcase: 0 */
|
/* eslint camelcase: 0 */
|
||||||
import { ChangeEvent, FormEvent, Component } from 'react';
|
import { ChangeEvent, ComponentProps, FormEvent, Component } from 'react';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
import { useRouter, type RouterHistory } from '@tanstack/react-router';
|
||||||
import {
|
import {
|
||||||
InfoTooltip,
|
InfoTooltip,
|
||||||
Button,
|
Button,
|
||||||
@@ -64,7 +64,8 @@ import { CHART_WIDTH, CHART_HEIGHT } from 'src/dashboard/constants';
|
|||||||
// Session storage key for recent dashboard
|
// Session storage key for recent dashboard
|
||||||
const SK_DASHBOARD_ID = 'save_chart_recent_dashboard';
|
const SK_DASHBOARD_ID = 'save_chart_recent_dashboard';
|
||||||
|
|
||||||
interface SaveModalProps extends RouteComponentProps {
|
interface SaveModalProps {
|
||||||
|
history: RouterHistory;
|
||||||
addDangerToast: (msg: string) => void;
|
addDangerToast: (msg: string) => void;
|
||||||
actions: Record<string, any>;
|
actions: Record<string, any>;
|
||||||
form_data?: Record<string, any>;
|
form_data?: Record<string, any>;
|
||||||
@@ -836,7 +837,18 @@ function mapStateToProps({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps)(withTheme(SaveModal)));
|
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;
|
||||||
|
|
||||||
// User for testing purposes need to revisit once we convert this to functional component
|
// User for testing purposes need to revisit once we convert this to functional component
|
||||||
export { SaveModal as PureSaveModal };
|
export { SaveModal as PureSaveModal };
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { Route } from 'react-router-dom';
|
import { useLocation } from '@tanstack/react-router';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { DatasourceType, JsonObject, SupersetClient } from '@superset-ui/core';
|
import { DatasourceType, JsonObject, SupersetClient } from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
@@ -315,16 +315,19 @@ test('Edit dataset should be disabled when user is not admin', async () => {
|
|||||||
test('Click on View in SQL Lab', async () => {
|
test('Click on View in SQL Lab', async () => {
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
|
|
||||||
const { queryByTestId, getByTestId } = render(
|
// 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(
|
||||||
<>
|
<>
|
||||||
<Route
|
<MockSqlLabRoute />
|
||||||
path="/sqllab"
|
|
||||||
render={({ location }) => (
|
|
||||||
<div data-test="mock-sqllab-route">
|
|
||||||
{JSON.stringify(location.state)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<DatasourceControl {...props} />
|
<DatasourceControl {...props} />
|
||||||
</>,
|
</>,
|
||||||
{
|
{
|
||||||
@@ -338,14 +341,14 @@ test('Click on View in SQL Lab', async () => {
|
|||||||
|
|
||||||
await userEvent.click(screen.getByText('View in SQL Lab'));
|
await userEvent.click(screen.getByText('View in SQL Lab'));
|
||||||
|
|
||||||
expect(getByTestId('mock-sqllab-route')).toBeInTheDocument();
|
expect(await findByTestId('mock-sqllab-route')).toBeInTheDocument();
|
||||||
expect(JSON.parse(`${getByTestId('mock-sqllab-route').textContent}`)).toEqual(
|
expect(JSON.parse(`${getByTestId('mock-sqllab-route').textContent}`)).toEqual(
|
||||||
{
|
expect.objectContaining({
|
||||||
requestedQuery: {
|
requestedQuery: {
|
||||||
datasourceKey: `${mockDatasource.id}__${mockDatasource.type}`,
|
datasourceKey: `${mockDatasource.id}__${mockDatasource.type}`,
|
||||||
sql: mockDatasource.sql,
|
sql: mockDatasource.sql,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ import ViewQueryModalFooter from 'src/explore/components/controls/ViewQueryModal
|
|||||||
import ViewQuery from 'src/explore/components/controls/ViewQuery';
|
import ViewQuery from 'src/explore/components/controls/ViewQuery';
|
||||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||||
import { safeStringify } from 'src/utils/safeStringify';
|
import { safeStringify } from 'src/utils/safeStringify';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
|
|
||||||
// Extended Datasource interface with all properties used in this component
|
// Extended Datasource interface with all properties used in this component
|
||||||
interface ExtendedDatasource extends Datasource {
|
interface ExtendedDatasource extends Datasource {
|
||||||
@@ -415,10 +415,8 @@ class DatasourceControl extends PureComponent<
|
|||||||
key: VIEW_IN_SQL_LAB,
|
key: VIEW_IN_SQL_LAB,
|
||||||
label: (
|
label: (
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to="/sqllab"
|
||||||
pathname: '/sqllab',
|
state={{ requestedQuery }}
|
||||||
state: { requestedQuery },
|
|
||||||
}}
|
|
||||||
onClick={preventRouterLinkWhileMetaClicked}
|
onClick={preventRouterLinkWhileMetaClicked}
|
||||||
>
|
>
|
||||||
{t('View in SQL Lab')}
|
{t('View in SQL Lab')}
|
||||||
@@ -472,10 +470,8 @@ class DatasourceControl extends PureComponent<
|
|||||||
key: VIEW_IN_SQL_LAB,
|
key: VIEW_IN_SQL_LAB,
|
||||||
label: (
|
label: (
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to="/sqllab"
|
||||||
pathname: '/sqllab',
|
state={{ requestedQuery }}
|
||||||
state: { requestedQuery },
|
|
||||||
}}
|
|
||||||
onClick={preventRouterLinkWhileMetaClicked}
|
onClick={preventRouterLinkWhileMetaClicked}
|
||||||
>
|
>
|
||||||
{t('View in SQL Lab')}
|
{t('View in SQL Lab')}
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ import { RootState } from 'src/dashboard/types';
|
|||||||
import ViewQuery, { ViewQueryProps } from './ViewQuery';
|
import ViewQuery, { ViewQueryProps } from './ViewQuery';
|
||||||
|
|
||||||
const mockHistoryPush = jest.fn();
|
const mockHistoryPush = jest.fn();
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('@tanstack/react-router', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('@tanstack/react-router'),
|
||||||
useHistory: () => ({
|
useRouter: () => ({
|
||||||
push: mockHistoryPush,
|
history: {
|
||||||
|
push: mockHistoryPush,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -162,13 +164,10 @@ test('navigates to SQL Lab when View in SQL Lab button is clicked', () => {
|
|||||||
const viewInSQLLabButton = screen.getByText('View in SQL Lab');
|
const viewInSQLLabButton = screen.getByText('View in SQL Lab');
|
||||||
fireEvent.click(viewInSQLLabButton);
|
fireEvent.click(viewInSQLLabButton);
|
||||||
|
|
||||||
expect(mockHistoryPush).toHaveBeenCalledWith({
|
expect(mockHistoryPush).toHaveBeenCalledWith('/sqllab', {
|
||||||
pathname: '/sqllab',
|
requestedQuery: {
|
||||||
state: {
|
datasourceKey: mockProps.datasource,
|
||||||
requestedQuery: {
|
sql: mockProps.sql,
|
||||||
datasourceKey: mockProps.datasource,
|
|
||||||
sql: mockProps.sql,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ import CodeSyntaxHighlighter, {
|
|||||||
SupportedLanguage,
|
SupportedLanguage,
|
||||||
preloadLanguages,
|
preloadLanguages,
|
||||||
} from '@superset-ui/core/components/CodeSyntaxHighlighter';
|
} from '@superset-ui/core/components/CodeSyntaxHighlighter';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import { ExplorePageState } from 'src/explore/types';
|
import { ExplorePageState } from 'src/explore/types';
|
||||||
|
|
||||||
export interface ViewQueryProps {
|
export interface ViewQueryProps {
|
||||||
@@ -86,7 +87,7 @@ const ViewQuery: FC<ViewQueryProps> = props => {
|
|||||||
);
|
);
|
||||||
const [formattedSQL, setFormattedSQL] = useState<string>();
|
const [formattedSQL, setFormattedSQL] = useState<string>();
|
||||||
const [showFormatSQL, setShowFormatSQL] = useState(true);
|
const [showFormatSQL, setShowFormatSQL] = useState(true);
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const currentSQL = (showFormatSQL ? formattedSQL : sql) ?? sql;
|
const currentSQL = (showFormatSQL ? formattedSQL : sql) ?? sql;
|
||||||
const canAccessSQLLab = useSelector((state: RootState) =>
|
const canAccessSQLLab = useSelector((state: RootState) =>
|
||||||
findPermission('menu_access', 'SQL Lab', state.user?.roles),
|
findPermission('menu_access', 'SQL Lab', state.user?.roles),
|
||||||
@@ -147,10 +148,10 @@ const ViewQuery: FC<ViewQueryProps> = props => {
|
|||||||
'_blank',
|
'_blank',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
history.push({ pathname: '/sqllab', state: { requestedQuery } });
|
pushAppHref(router, '/sqllab', { requestedQuery });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[history, datasource, currentSQL],
|
[router, datasource, currentSQL],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import { isObject } from 'lodash';
|
|||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
import { Button } from '@superset-ui/core/components';
|
import { Button } from '@superset-ui/core/components';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
|
|
||||||
interface SimpleDataSource {
|
interface SimpleDataSource {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -44,7 +45,7 @@ const ViewQueryModalFooter: FC<ViewQueryModalFooterProps> = (props: {
|
|||||||
changeDatasource: () => void;
|
changeDatasource: () => void;
|
||||||
datasource: SimpleDataSource;
|
datasource: SimpleDataSource;
|
||||||
}) => {
|
}) => {
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const viewInSQLLab = (
|
const viewInSQLLab = (
|
||||||
openInNewWindow: boolean,
|
openInNewWindow: boolean,
|
||||||
id: string,
|
id: string,
|
||||||
@@ -58,11 +59,8 @@ const ViewQueryModalFooter: FC<ViewQueryModalFooterProps> = (props: {
|
|||||||
if (openInNewWindow) {
|
if (openInNewWindow) {
|
||||||
SupersetClient.postForm('/sqllab/', payload);
|
SupersetClient.postForm('/sqllab/', payload);
|
||||||
} else {
|
} else {
|
||||||
history.push({
|
pushAppHref(router, '/sqllab', {
|
||||||
pathname: '/sqllab',
|
requestedQuery: payload,
|
||||||
state: {
|
|
||||||
requestedQuery: payload,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { t } from '@apache-superset/core/translation';
|
|||||||
import { css, useTheme } from '@apache-superset/core/theme';
|
import { css, useTheme } from '@apache-superset/core/theme';
|
||||||
import { MenuItem } from '@superset-ui/core/components/Menu';
|
import { MenuItem } from '@superset-ui/core/components/Menu';
|
||||||
import { Icons } from '@superset-ui/core/components/Icons';
|
import { Icons } from '@superset-ui/core/components/Icons';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
|
|
||||||
export interface DashboardsMenuProps {
|
export interface DashboardsMenuProps {
|
||||||
chartId?: number;
|
chartId?: number;
|
||||||
@@ -45,7 +45,10 @@ export const useDashboardsMenuItems = ({
|
|||||||
);
|
);
|
||||||
}, [dashboards, searchTerm]);
|
}, [dashboards, searchTerm]);
|
||||||
|
|
||||||
const urlQueryString = chartId ? `?focused_chart=${chartId}` : '';
|
const urlSearch = useMemo(
|
||||||
|
() => (chartId ? { focused_chart: String(chartId) } : undefined),
|
||||||
|
[chartId],
|
||||||
|
);
|
||||||
const noResults = dashboards.length === 0;
|
const noResults = dashboards.length === 0;
|
||||||
const noResultsFound = searchTerm && filteredDashboards.length === 0;
|
const noResultsFound = searchTerm && filteredDashboards.length === 0;
|
||||||
|
|
||||||
@@ -72,7 +75,8 @@ export const useDashboardsMenuItems = ({
|
|||||||
<Link
|
<Link
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferer noopener"
|
rel="noreferer noopener"
|
||||||
to={`/superset/dashboard/${dashboard.id}${urlQueryString}`}
|
to={`/superset/dashboard/${dashboard.id}`}
|
||||||
|
search={urlSearch}
|
||||||
css={css`
|
css={css`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -102,7 +106,7 @@ export const useDashboardsMenuItems = ({
|
|||||||
return items;
|
return items;
|
||||||
}, [
|
}, [
|
||||||
filteredDashboards,
|
filteredDashboards,
|
||||||
urlQueryString,
|
urlSearch,
|
||||||
noResults,
|
noResults,
|
||||||
noResultsFound,
|
noResultsFound,
|
||||||
theme.sizeUnit,
|
theme.sizeUnit,
|
||||||
|
|||||||
@@ -16,10 +16,13 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
|
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
|
||||||
import { css } from '@apache-superset/core/theme';
|
import { css } from '@apache-superset/core/theme';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
|
import { parseSearch } from 'src/router/searchParams';
|
||||||
import {
|
import {
|
||||||
ConfirmStatusChange,
|
ConfirmStatusChange,
|
||||||
Button,
|
Button,
|
||||||
@@ -55,6 +58,20 @@ interface ChartCardProps {
|
|||||||
getData?: (tab: TableTab) => void;
|
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({
|
export default function ChartCard({
|
||||||
chart,
|
chart,
|
||||||
hasPerm,
|
hasPerm,
|
||||||
@@ -72,7 +89,7 @@ export default function ChartCard({
|
|||||||
handleBulkChartExport,
|
handleBulkChartExport,
|
||||||
getData,
|
getData,
|
||||||
}: ChartCardProps) {
|
}: ChartCardProps) {
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const canEdit = hasPerm('can_write');
|
const canEdit = hasPerm('can_write');
|
||||||
const canDelete = hasPerm('can_write');
|
const canDelete = hasPerm('can_write');
|
||||||
const canExport = hasPerm('can_export');
|
const canExport = hasPerm('can_export');
|
||||||
@@ -170,7 +187,7 @@ export default function ChartCard({
|
|||||||
<CardStyles
|
<CardStyles
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!bulkSelectEnabled && chart.url) {
|
if (!bulkSelectEnabled && chart.url) {
|
||||||
history.push(chart.url);
|
pushAppHref(router, chart.url);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -192,7 +209,7 @@ export default function ChartCard({
|
|||||||
description={t('Modified %s', chart.changed_on_delta_humanized)}
|
description={t('Modified %s', chart.changed_on_delta_humanized)}
|
||||||
coverLeft={<FacePile users={chart.owners || []} />}
|
coverLeft={<FacePile users={chart.owners || []} />}
|
||||||
coverRight={<Label>{chart.datasource_name_text}</Label>}
|
coverRight={<Label>{chart.datasource_name_text}</Label>}
|
||||||
linkComponent={Link}
|
linkComponent={CardLink}
|
||||||
actions={
|
actions={
|
||||||
<ListViewCard.Actions
|
<ListViewCard.Actions
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import {
|
import {
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
SupersetClient,
|
SupersetClient,
|
||||||
@@ -68,7 +68,7 @@ afterAll(() => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<StandaloneRouter>
|
||||||
<DashboardCard
|
<DashboardCard
|
||||||
dashboard={mockDashboard}
|
dashboard={mockDashboard}
|
||||||
hasPerm={mockHasPerm}
|
hasPerm={mockHasPerm}
|
||||||
@@ -80,7 +80,7 @@ beforeEach(() => {
|
|||||||
handleBulkDashboardExport={mockHandleBulkDashboardExport}
|
handleBulkDashboardExport={mockHandleBulkDashboardExport}
|
||||||
onDelete={mockOnDelete}
|
onDelete={mockOnDelete}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>,
|
</StandaloneRouter>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,6 +129,7 @@ test('should fetch thumbnail when dashboard has no thumbnail URL and feature fla
|
|||||||
handleBulkDashboardExport={() => {}}
|
handleBulkDashboardExport={() => {}}
|
||||||
onDelete={() => {}}
|
onDelete={() => {}}
|
||||||
/>,
|
/>,
|
||||||
|
{ useRouter: true },
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockGet).toHaveBeenCalledWith({
|
expect(mockGet).toHaveBeenCalledWith({
|
||||||
|
|||||||
@@ -16,8 +16,10 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, type ReactNode } from 'react';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
|
import { parseSearch } from 'src/router/searchParams';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import {
|
import {
|
||||||
isFeatureEnabled,
|
isFeatureEnabled,
|
||||||
@@ -53,6 +55,20 @@ interface DashboardCardProps {
|
|||||||
onDelete: (dashboard: Dashboard) => void;
|
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({
|
function DashboardCard({
|
||||||
dashboard,
|
dashboard,
|
||||||
hasPerm,
|
hasPerm,
|
||||||
@@ -65,7 +81,7 @@ function DashboardCard({
|
|||||||
handleBulkDashboardExport,
|
handleBulkDashboardExport,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: DashboardCardProps) {
|
}: DashboardCardProps) {
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const canEdit = hasPerm('can_write');
|
const canEdit = hasPerm('can_write');
|
||||||
const canDelete = hasPerm('can_write');
|
const canDelete = hasPerm('can_write');
|
||||||
const canExport = hasPerm('can_export');
|
const canExport = hasPerm('can_export');
|
||||||
@@ -154,7 +170,7 @@ function DashboardCard({
|
|||||||
<CardStyles
|
<CardStyles
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!bulkSelectEnabled) {
|
if (!bulkSelectEnabled) {
|
||||||
history.push(dashboard.url);
|
pushAppHref(router, dashboard.url);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -170,7 +186,7 @@ function DashboardCard({
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
url={bulkSelectEnabled ? undefined : dashboard.url}
|
url={bulkSelectEnabled ? undefined : dashboard.url}
|
||||||
linkComponent={Link}
|
linkComponent={CardLink}
|
||||||
imgURL={thumbnailUrl}
|
imgURL={thumbnailUrl}
|
||||||
imgFallbackURL={assetUrl(
|
imgFallbackURL={assetUrl(
|
||||||
'/static/assets/images/dashboard-card-fallback.svg',
|
'/static/assets/images/dashboard-card-fallback.svg',
|
||||||
|
|||||||
@@ -45,10 +45,12 @@ jest.mock('@superset-ui/core', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const mockHistoryPush = jest.fn();
|
const mockHistoryPush = jest.fn();
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('@tanstack/react-router', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('@tanstack/react-router'),
|
||||||
useHistory: () => ({
|
useRouter: () => ({
|
||||||
push: mockHistoryPush,
|
history: {
|
||||||
|
push: mockHistoryPush,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { CheckboxChangeEvent } from '@superset-ui/core/components/Checkbox/types';
|
import { CheckboxChangeEvent } from '@superset-ui/core/components/Checkbox/types';
|
||||||
|
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||||
import Tabs from '@superset-ui/core/components/Tabs';
|
import Tabs from '@superset-ui/core/components/Tabs';
|
||||||
import {
|
import {
|
||||||
@@ -751,7 +752,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||||||
(DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
|
(DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
|
||||||
)?.parameters !== undefined;
|
)?.parameters !== undefined;
|
||||||
const showDBError = validationErrors || dbErrors;
|
const showDBError = validationErrors || dbErrors;
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
|
|
||||||
const dbModel: DatabaseForm =
|
const dbModel: DatabaseForm =
|
||||||
// TODO: we need a centralized engine in one place
|
// TODO: we need a centralized engine in one place
|
||||||
@@ -887,7 +888,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const redirectURL = (url: string) => {
|
const redirectURL = (url: string) => {
|
||||||
history.push(url);
|
pushAppHref(router, url);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Database import logic
|
// Database import logic
|
||||||
@@ -1875,8 +1876,8 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchAndSetDB();
|
fetchAndSetDB();
|
||||||
// redirectURL() delegates to history.push; React Router's basename
|
// redirectURL() prefixes the application root via pushAppHref,
|
||||||
// already prefixes the application root, so pass a relative path.
|
// so pass a root-relative path.
|
||||||
redirectURL('/sqllab?db=true');
|
redirectURL('/sqllab?db=true');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { styled } from '@apache-superset/core/theme';
|
import { styled } from '@apache-superset/core/theme';
|
||||||
import { EmptyState } from '@superset-ui/core/components';
|
import { EmptyState } from '@superset-ui/core/components';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
padding: ${({ theme }) => theme.sizeUnit * 8}px
|
padding: ${({ theme }) => theme.sizeUnit * 8}px
|
||||||
|
|||||||
@@ -24,11 +24,16 @@ import {
|
|||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import Footer from 'src/features/datasets/AddDataset/Footer';
|
import Footer from 'src/features/datasets/AddDataset/Footer';
|
||||||
|
|
||||||
|
const mockNavigate = jest.fn();
|
||||||
const mockHistoryPush = jest.fn();
|
const mockHistoryPush = jest.fn();
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('@tanstack/react-router', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('@tanstack/react-router'),
|
||||||
useHistory: () => ({
|
useNavigate: () => mockNavigate,
|
||||||
push: mockHistoryPush,
|
useRouter: () => ({
|
||||||
|
history: {
|
||||||
|
push: mockHistoryPush,
|
||||||
|
back: jest.fn(),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -173,7 +178,9 @@ describe('Footer', () => {
|
|||||||
schema: 'public',
|
schema: 'public',
|
||||||
table_name: 'real_info',
|
table_name: 'real_info',
|
||||||
});
|
});
|
||||||
expect(mockHistoryPush).toHaveBeenCalledWith('/tablemodelview/list/');
|
expect(mockNavigate).toHaveBeenCalledWith({
|
||||||
|
to: '/tablemodelview/list/',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,6 +199,7 @@ describe('Footer', () => {
|
|||||||
expect(mockCreateResource).toHaveBeenCalled();
|
expect(mockCreateResource).toHaveBeenCalled();
|
||||||
// Should not navigate if creation failed
|
// Should not navigate if creation failed
|
||||||
expect(mockHistoryPush).not.toHaveBeenCalled();
|
expect(mockHistoryPush).not.toHaveBeenCalled();
|
||||||
|
expect(mockNavigate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useNavigate, useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DropdownButton,
|
DropdownButton,
|
||||||
@@ -61,7 +62,8 @@ function Footer({
|
|||||||
hasColumns = false,
|
hasColumns = false,
|
||||||
datasets,
|
datasets,
|
||||||
}: FooterProps) {
|
}: FooterProps) {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
|
const router = useRouter();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { createResource, state } = useSingleViewResource<
|
const { createResource, state } = useSingleViewResource<
|
||||||
Partial<DatasetObject>
|
Partial<DatasetObject>
|
||||||
@@ -87,7 +89,7 @@ function Footer({
|
|||||||
const logAction = createLogAction(datasetObject);
|
const logAction = createLogAction(datasetObject);
|
||||||
logEvent(logAction, datasetObject);
|
logEvent(logAction, datasetObject);
|
||||||
}
|
}
|
||||||
history.goBack();
|
router.history.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
const tooltipText = t('Select a database table.');
|
const tooltipText = t('Select a database table.');
|
||||||
@@ -108,9 +110,12 @@ function Footer({
|
|||||||
logEvent(LOG_ACTIONS_DATASET_CREATION_SUCCESS, datasetObject);
|
logEvent(LOG_ACTIONS_DATASET_CREATION_SUCCESS, datasetObject);
|
||||||
// When a dataset is created the response we get is its ID number
|
// When a dataset is created the response we get is its ID number
|
||||||
if (createChart) {
|
if (createChart) {
|
||||||
history.push(`/chart/add/?dataset=${datasetObject.table_name}`);
|
pushAppHref(
|
||||||
|
router,
|
||||||
|
`/chart/add/?dataset=${datasetObject.table_name}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
history.push('/tablemodelview/list/');
|
navigate({ to: '/tablemodelview/list/' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,14 +25,6 @@ import DatasetPanelComponent from 'src/features/datasets/AddDataset/DatasetPanel
|
|||||||
import RightPanel from 'src/features/datasets/AddDataset/RightPanel';
|
import RightPanel from 'src/features/datasets/AddDataset/RightPanel';
|
||||||
import Footer from 'src/features/datasets/AddDataset/Footer';
|
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
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('DatasetLayout', () => {
|
describe('DatasetLayout', () => {
|
||||||
test('renders nothing when no components are passed in', () => {
|
test('renders nothing when no components are passed in', () => {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import { extendedDayjs } from '@superset-ui/core/utils/dates';
|
|||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { styled } from '@apache-superset/core/theme';
|
import { styled } from '@apache-superset/core/theme';
|
||||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
|
import { parseSearch } from 'src/router/searchParams';
|
||||||
import { ListViewCard } from '@superset-ui/core/components';
|
import { ListViewCard } from '@superset-ui/core/components';
|
||||||
import { Dashboard, SavedQueryObject, TableTab } from 'src/views/CRUD/types';
|
import { Dashboard, SavedQueryObject, TableTab } from 'src/views/CRUD/types';
|
||||||
import { ActivityData, LoadingCards } from 'src/pages/Home';
|
import { ActivityData, LoadingCards } from 'src/pages/Home';
|
||||||
@@ -186,9 +187,14 @@ export default function ActivityTable({
|
|||||||
return activities.map((entity: ActivityObject) => {
|
return activities.map((entity: ActivityObject) => {
|
||||||
const url = getEntityUrl(entity);
|
const url = getEntityUrl(entity);
|
||||||
const lastActionOn = getEntityLastActionOn(entity);
|
const lastActionOn = getEntityLastActionOn(entity);
|
||||||
|
// Entity URLs come from backend data and may carry a query string.
|
||||||
|
const [pathname, queryString] = (url || '').split('?');
|
||||||
return (
|
return (
|
||||||
<CardStyles key={url}>
|
<CardStyles key={url}>
|
||||||
<Link to={url}>
|
<Link
|
||||||
|
to={pathname}
|
||||||
|
search={queryString ? parseSearch(queryString) : undefined}
|
||||||
|
>
|
||||||
<ListViewCard
|
<ListViewCard
|
||||||
cover={<></>}
|
cover={<></>}
|
||||||
url={url}
|
url={url}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ import {
|
|||||||
setItem,
|
setItem,
|
||||||
} from 'src/utils/localStorageHelpers';
|
} from 'src/utils/localStorageHelpers';
|
||||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import { Filter, TableTab } from 'src/views/CRUD/types';
|
import { Filter, TableTab } from 'src/views/CRUD/types';
|
||||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||||
import { User } from 'src/types/bootstrapTypes';
|
import { User } from 'src/types/bootstrapTypes';
|
||||||
@@ -71,7 +72,7 @@ function ChartTable({
|
|||||||
otherTabFilters,
|
otherTabFilters,
|
||||||
otherTabTitle,
|
otherTabTitle,
|
||||||
}: ChartTableProps) {
|
}: ChartTableProps) {
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const initialTab = getItem(
|
const initialTab = getItem(
|
||||||
LocalStorageKeys.HomepageChartFilter,
|
LocalStorageKeys.HomepageChartFilter,
|
||||||
TableTab.Other,
|
TableTab.Other,
|
||||||
@@ -215,7 +216,7 @@ function ChartTable({
|
|||||||
'Yes',
|
'Yes',
|
||||||
)},value:!t))`
|
)},value:!t))`
|
||||||
: '/chart/list/';
|
: '/chart/list/';
|
||||||
history.push(target);
|
pushAppHref(router, target);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ import {
|
|||||||
waitFor,
|
waitFor,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
import { createMemoryHistory } from 'history';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { Router } from 'react-router-dom';
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import * as hooks from 'src/views/CRUD/hooks';
|
import * as hooks from 'src/views/CRUD/hooks';
|
||||||
@@ -105,7 +104,6 @@ const defaultProps = {
|
|||||||
otherTabTitle: 'Examples',
|
otherTabTitle: 'Examples',
|
||||||
};
|
};
|
||||||
|
|
||||||
const history = createMemoryHistory();
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
dashboards: (state = { dashboards: [] }) => state,
|
dashboards: (state = { dashboards: [] }) => state,
|
||||||
@@ -156,9 +154,9 @@ beforeEach(() => {
|
|||||||
|
|
||||||
test('renders loading state initially', () => {
|
test('renders loading state initially', () => {
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<DashboardTable {...defaultProps} />
|
<DashboardTable {...defaultProps} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ store },
|
{ store },
|
||||||
);
|
);
|
||||||
expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument();
|
expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument();
|
||||||
@@ -166,9 +164,9 @@ test('renders loading state initially', () => {
|
|||||||
|
|
||||||
test('renders empty state when no dashboards', async () => {
|
test('renders empty state when no dashboards', async () => {
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<DashboardTable {...defaultProps} />
|
<DashboardTable {...defaultProps} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ store },
|
{ store },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -194,9 +192,9 @@ test('renders dashboard cards when data is loaded', async () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<DashboardTable {...defaultProps} mine={mockDashboards} />
|
<DashboardTable {...defaultProps} mine={mockDashboards} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ store },
|
{ store },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -214,9 +212,9 @@ test('switches to Mine tab correctly', async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<DashboardTable {...props} />
|
<DashboardTable {...props} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ store },
|
{ store },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -235,9 +233,9 @@ test('handles create dashboard button click', async () => {
|
|||||||
} as Location);
|
} as Location);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<DashboardTable {...defaultProps} />
|
<DashboardTable {...defaultProps} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ store },
|
{ store },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -255,9 +253,9 @@ test('switches to Other tab when available', async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<DashboardTable {...props} />
|
<DashboardTable {...props} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ store },
|
{ store },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -299,9 +297,9 @@ test('handles bulk dashboard export with correct ID and shows spinner', async ()
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<DashboardTable {...props} />
|
<DashboardTable {...props} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ store },
|
{ store },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -362,9 +360,9 @@ test('handles dashboard deletion confirmation', async () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<DashboardTable {...props} />
|
<DashboardTable {...props} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ store },
|
{ store },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -434,9 +432,9 @@ test('passes correct parameters to handleDashboardDelete for Other tab', async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Router history={history}>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<DashboardTable {...props} />
|
<DashboardTable {...props} />
|
||||||
</Router>,
|
</StandaloneRouter>,
|
||||||
{ store },
|
{ store },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import { SupersetClient } from '@superset-ui/core';
|
|||||||
import { useFavoriteStatus, useListViewResource } from 'src/views/CRUD/hooks';
|
import { useFavoriteStatus, useListViewResource } from 'src/views/CRUD/hooks';
|
||||||
import { Dashboard, DashboardTableProps, TableTab } from 'src/views/CRUD/types';
|
import { Dashboard, DashboardTableProps, TableTab } from 'src/views/CRUD/types';
|
||||||
import handleResourceExport from 'src/utils/export';
|
import handleResourceExport from 'src/utils/export';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import {
|
import {
|
||||||
getItem,
|
getItem,
|
||||||
LocalStorageKeys,
|
LocalStorageKeys,
|
||||||
@@ -56,7 +57,7 @@ function DashboardTable({
|
|||||||
otherTabFilters,
|
otherTabFilters,
|
||||||
otherTabTitle,
|
otherTabTitle,
|
||||||
}: DashboardTableProps) {
|
}: DashboardTableProps) {
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
const defaultTab = getItem(
|
const defaultTab = getItem(
|
||||||
LocalStorageKeys.HomepageDashboardFilter,
|
LocalStorageKeys.HomepageDashboardFilter,
|
||||||
TableTab.Other,
|
TableTab.Other,
|
||||||
@@ -216,7 +217,7 @@ function DashboardTable({
|
|||||||
'Yes',
|
'Yes',
|
||||||
)},value:!t))`
|
)},value:!t))`
|
||||||
: '/dashboard/list/';
|
: '/dashboard/list/';
|
||||||
history.push(target);
|
pushAppHref(router, target);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import { getUrlParam } from 'src/utils/urlUtils';
|
|||||||
import { MainNav, MenuItem } from '@superset-ui/core/components/Menu';
|
import { MainNav, MenuItem } from '@superset-ui/core/components/Menu';
|
||||||
import { Tooltip, Grid, Row, Col, Image } from '@superset-ui/core/components';
|
import { Tooltip, Grid, Row, Col, Image } from '@superset-ui/core/components';
|
||||||
import { GenericLink } from 'src/components';
|
import { GenericLink } from 'src/components';
|
||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from '@tanstack/react-router';
|
||||||
|
import { parseSearch } from 'src/router/searchParams';
|
||||||
import { Icons } from '@superset-ui/core/components/Icons';
|
import { Icons } from '@superset-ui/core/components/Icons';
|
||||||
import { Typography } from '@superset-ui/core/components/Typography';
|
import { Typography } from '@superset-ui/core/components/Typography';
|
||||||
import { useUiConfig } from 'src/components/UiConfigContext';
|
import { useUiConfig } from 'src/components/UiConfigContext';
|
||||||
@@ -244,12 +245,19 @@ export function Menu({
|
|||||||
isFrontendRoute,
|
isFrontendRoute,
|
||||||
}: MenuObjectProps): MenuItem => {
|
}: MenuObjectProps): MenuItem => {
|
||||||
if (url && isFrontendRoute) {
|
if (url && isFrontendRoute) {
|
||||||
|
// Menu URLs come from backend data and may carry a query string.
|
||||||
|
const [pathname, queryString] = url.split('?');
|
||||||
return {
|
return {
|
||||||
key: label,
|
key: label,
|
||||||
label: (
|
label: (
|
||||||
<NavLink role="button" to={url} activeClassName="is-active">
|
<Link
|
||||||
|
role="button"
|
||||||
|
to={pathname}
|
||||||
|
search={queryString ? parseSearch(queryString) : undefined}
|
||||||
|
activeProps={{ className: 'is-active' }}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</NavLink>
|
</Link>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -266,12 +274,20 @@ export function Menu({
|
|||||||
if (typeof child === 'string' && child === '-' && label !== 'Data') {
|
if (typeof child === 'string' && child === '-' && label !== 'Data') {
|
||||||
childItems.push({ type: 'divider', key: `divider-${index1}` });
|
childItems.push({ type: 'divider', key: `divider-${index1}` });
|
||||||
} else if (typeof child !== 'string') {
|
} else if (typeof child !== 'string') {
|
||||||
|
const [childPathname, childQueryString] = (child.url || '').split('?');
|
||||||
childItems.push({
|
childItems.push({
|
||||||
key: `${child.label}`,
|
key: `${child.label}`,
|
||||||
label: child.isFrontendRoute ? (
|
label: child.isFrontendRoute ? (
|
||||||
<NavLink to={child.url || ''} exact activeClassName="is-active">
|
<Link
|
||||||
|
to={childPathname}
|
||||||
|
search={
|
||||||
|
childQueryString ? parseSearch(childQueryString) : undefined
|
||||||
|
}
|
||||||
|
activeOptions={{ exact: true }}
|
||||||
|
activeProps={{ className: 'is-active' }}
|
||||||
|
>
|
||||||
{child.label}
|
{child.label}
|
||||||
</NavLink>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Link href={child.url}>{child.label}</Typography.Link>
|
<Typography.Link href={child.url}>{child.label}</Typography.Link>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -16,10 +16,18 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect, FC, PureComponent, useMemo } from 'react';
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
FC,
|
||||||
|
PureComponent,
|
||||||
|
ReactNode,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
|
import { parseSearch } from 'src/router/searchParams';
|
||||||
import { useQueryParams, BooleanParam } from 'use-query-params';
|
import { useQueryParams, BooleanParam } from 'use-query-params';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
@@ -102,6 +110,26 @@ 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 = ({
|
const RightMenu = ({
|
||||||
align,
|
align,
|
||||||
settings,
|
settings,
|
||||||
@@ -409,7 +437,7 @@ const RightMenu = ({
|
|||||||
items.push({
|
items.push({
|
||||||
key: menu.label,
|
key: menu.label,
|
||||||
label: isFrontendRoute(menu.url) ? (
|
label: isFrontendRoute(menu.url) ? (
|
||||||
<Link to={menu.url || ''}>{menu.label}</Link>
|
<RouterLink url={menu.url || ''}>{menu.label}</RouterLink>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Link href={ensureAppRoot(menu.url || '')}>
|
<Typography.Link href={ensureAppRoot(menu.url || '')}>
|
||||||
{menu.label}
|
{menu.label}
|
||||||
@@ -425,7 +453,7 @@ const RightMenu = ({
|
|||||||
items.push({
|
items.push({
|
||||||
key: menu.label,
|
key: menu.label,
|
||||||
label: isFrontendRoute(menu.url) ? (
|
label: isFrontendRoute(menu.url) ? (
|
||||||
<Link to={menu.url || ''}>{menu.label}</Link>
|
<RouterLink url={menu.url || ''}>{menu.label}</RouterLink>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Link href={ensureAppRoot(menu.url || '')}>
|
<Typography.Link href={ensureAppRoot(menu.url || '')}>
|
||||||
{menu.label}
|
{menu.label}
|
||||||
@@ -460,7 +488,7 @@ const RightMenu = ({
|
|||||||
sectionItems.push({
|
sectionItems.push({
|
||||||
key: child.label,
|
key: child.label,
|
||||||
label: isFrontendRoute(child.url) ? (
|
label: isFrontendRoute(child.url) ? (
|
||||||
<Link to={child.url || ''}>{menuItemDisplay}</Link>
|
<RouterLink url={child.url || ''}>{menuItemDisplay}</RouterLink>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Link
|
<Typography.Link
|
||||||
href={child.url || ''}
|
href={child.url || ''}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useCallback, useState, useEffect } from 'react';
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
import { styled, useTheme, css } from '@apache-superset/core/theme';
|
import { styled, useTheme, css } from '@apache-superset/core/theme';
|
||||||
@@ -206,7 +206,11 @@ export const SavedQueries = ({
|
|||||||
if (canEdit) {
|
if (canEdit) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: <Link to={`/sqllab?savedQueryId=${query.id}`}>{t('Edit')}</Link>,
|
label: (
|
||||||
|
<Link to="/sqllab" search={{ savedQueryId: String(query.id) }}>
|
||||||
|
{t('Edit')}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
@@ -278,7 +282,8 @@ export const SavedQueries = ({
|
|||||||
icon: <Icons.PlusOutlined iconSize="m" />,
|
icon: <Icons.PlusOutlined iconSize="m" />,
|
||||||
name: (
|
name: (
|
||||||
<Link
|
<Link
|
||||||
to="/sqllab?new=true"
|
to="/sqllab"
|
||||||
|
search={{ new: 'true' }}
|
||||||
css={css`
|
css={css`
|
||||||
&:hover {
|
&:hover {
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||||
import SubMenu, { ButtonProps } from './SubMenu';
|
import SubMenu, { ButtonProps } from './SubMenu';
|
||||||
|
|
||||||
@@ -63,9 +63,9 @@ const setup = (overrides: Record<string, any> = {}) => {
|
|||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
return render(
|
return render(
|
||||||
<BrowserRouter>
|
<StandaloneRouter>
|
||||||
<SubMenu {...props} />
|
<SubMenu {...props} />
|
||||||
</BrowserRouter>,
|
</StandaloneRouter>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
*/
|
*/
|
||||||
import { ReactNode, useState, useEffect, FunctionComponent } from 'react';
|
import { ReactNode, useState, useEffect, FunctionComponent } from 'react';
|
||||||
|
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useRouter } from '@tanstack/react-router';
|
||||||
|
import { parseSearch } from 'src/router/searchParams';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import {
|
import {
|
||||||
styled,
|
styled,
|
||||||
@@ -172,14 +173,9 @@ const SubMenuComponent: FunctionComponent<SubMenuProps> = props => {
|
|||||||
const [navRightStyle, setNavRightStyle] = useState('nav-right');
|
const [navRightStyle, setNavRightStyle] = useState('nav-right');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
let hasHistory = true;
|
// If no parent <RouterProvider> exists, useRouter returns undefined and
|
||||||
// If no parent <Router> component exists, useHistory throws an error
|
// we know not to use <Link> in render
|
||||||
try {
|
const hasHistory = !!useRouter({ warn: false });
|
||||||
useHistory();
|
|
||||||
} catch (err) {
|
|
||||||
// If error is thrown, we know not to use <Link> in render
|
|
||||||
hasHistory = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
@@ -223,11 +219,14 @@ const SubMenuComponent: FunctionComponent<SubMenuProps> = props => {
|
|||||||
role="tablist"
|
role="tablist"
|
||||||
items={props.tabs?.map(tab => {
|
items={props.tabs?.map(tab => {
|
||||||
if ((props.usesRouter || hasHistory) && !!tab.usesRouter) {
|
if ((props.usesRouter || hasHistory) && !!tab.usesRouter) {
|
||||||
|
// Tab URLs may carry a query string.
|
||||||
|
const [pathname, queryString] = (tab.url || '').split('?');
|
||||||
return {
|
return {
|
||||||
key: tab.label,
|
key: tab.label,
|
||||||
label: (
|
label: (
|
||||||
<Link
|
<Link
|
||||||
to={tab.url || ''}
|
to={pathname}
|
||||||
|
search={queryString ? parseSearch(queryString) : undefined}
|
||||||
role="tab"
|
role="tab"
|
||||||
id={tab.id || tab.name}
|
id={tab.id || tab.name}
|
||||||
data-test={tab['data-test']}
|
data-test={tab['data-test']}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
|
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
|
||||||
import { CardStyles } from 'src/views/CRUD/utils';
|
import { CardStyles } from 'src/views/CRUD/utils';
|
||||||
|
|||||||
@@ -18,10 +18,16 @@
|
|||||||
*/
|
*/
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { getClientErrorObject } from '@superset-ui/core';
|
import { getClientErrorObject } from '@superset-ui/core';
|
||||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
import {
|
||||||
import { useHistory } from 'react-router-dom';
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
type Dispatch,
|
||||||
|
type SetStateAction,
|
||||||
|
} from 'react';
|
||||||
|
import { useBlocker } from '@tanstack/react-router';
|
||||||
import { useBeforeUnload } from 'src/hooks/useBeforeUnload';
|
import { useBeforeUnload } from 'src/hooks/useBeforeUnload';
|
||||||
import type { Location, Action } from 'history';
|
|
||||||
|
|
||||||
type UseUnsavedChangesPromptProps = {
|
type UseUnsavedChangesPromptProps = {
|
||||||
hasUnsavedChanges: boolean;
|
hasUnsavedChanges: boolean;
|
||||||
@@ -36,16 +42,53 @@ export const useUnsavedChangesPrompt = ({
|
|||||||
isSaveModalVisible = false,
|
isSaveModalVisible = false,
|
||||||
manualSaveOnUnsavedChanges = false,
|
manualSaveOnUnsavedChanges = false,
|
||||||
}: UseUnsavedChangesPromptProps) => {
|
}: UseUnsavedChangesPromptProps) => {
|
||||||
const history = useHistory();
|
const [showModal, setShowModalState] = useState(false);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const showModalRef = useRef(showModal);
|
||||||
|
showModalRef.current = showModal;
|
||||||
|
|
||||||
const confirmNavigationRef = useRef<(() => void) | null>(null);
|
|
||||||
const unblockRef = useRef<() => void>(() => {});
|
|
||||||
const manualSaveRef = useRef(false); // Track if save was user-initiated (not via navigation)
|
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(() => {
|
const handleConfirmNavigation = useCallback(() => {
|
||||||
setShowModal(false);
|
setShowModalState(false);
|
||||||
confirmNavigationRef.current?.();
|
blockerRef.current.proceed?.();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSaveAndCloseModal = useCallback(async () => {
|
const handleSaveAndCloseModal = useCallback(async () => {
|
||||||
@@ -63,66 +106,19 @@ export const useUnsavedChangesPrompt = ({
|
|||||||
{ cause: err },
|
{ cause: err },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [manualSaveOnUnsavedChanges, onSave]);
|
}, [manualSaveOnUnsavedChanges, onSave, setShowModal]);
|
||||||
|
|
||||||
const triggerManualSave = useCallback(() => {
|
const triggerManualSave = useCallback(() => {
|
||||||
manualSaveRef.current = true;
|
manualSaveRef.current = true;
|
||||||
onSave();
|
onSave();
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!isSaveModalVisible && manualSaveRef.current) {
|
if (!isSaveModalVisible && manualSaveRef.current) {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
manualSaveRef.current = false;
|
manualSaveRef.current = false;
|
||||||
}
|
}
|
||||||
}, [isSaveModalVisible]);
|
}, [isSaveModalVisible, setShowModal]);
|
||||||
|
|
||||||
useBeforeUnload(hasUnsavedChanges);
|
useBeforeUnload(hasUnsavedChanges);
|
||||||
|
|
||||||
|
|||||||
@@ -16,152 +16,143 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { renderHook, act } from '@testing-library/react';
|
import { ReactNode } from 'react';
|
||||||
import { Router } from 'react-router-dom';
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||||
import { createMemoryHistory } from 'history';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { useUnsavedChangesPrompt } from '.';
|
import { useUnsavedChangesPrompt } from '.';
|
||||||
|
|
||||||
let history = createMemoryHistory({
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
initialEntries: ['/dashboard'],
|
<StandaloneRouter initialEntries={['/dashboard']}>
|
||||||
});
|
{children}
|
||||||
|
</StandaloneRouter>
|
||||||
beforeEach(() => {
|
|
||||||
history = createMemoryHistory({ initialEntries: ['/dashboard'] });
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<Router history={history}>{children}</Router>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
test('should not show modal initially', () => {
|
const setup = async ({ onSave = jest.fn() }: { onSave?: jest.Mock } = {}) => {
|
||||||
const { result } = renderHook(
|
const utils = renderHook(
|
||||||
() =>
|
() => ({
|
||||||
useUnsavedChangesPrompt({
|
prompt: useUnsavedChangesPrompt({
|
||||||
hasUnsavedChanges: true,
|
hasUnsavedChanges: true,
|
||||||
onSave: jest.fn(),
|
onSave,
|
||||||
}),
|
}),
|
||||||
|
router: useRouter(),
|
||||||
|
}),
|
||||||
{ wrapper },
|
{ wrapper },
|
||||||
);
|
);
|
||||||
|
// the router mounts asynchronously before rendering its children
|
||||||
|
await waitFor(() => expect(utils.result.current).toBeTruthy());
|
||||||
|
return utils;
|
||||||
|
};
|
||||||
|
|
||||||
expect(result.current.showModal).toBe(false);
|
test('should not show modal initially', async () => {
|
||||||
|
const { result } = await setup();
|
||||||
|
|
||||||
|
expect(result.current.prompt.showModal).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should block navigation and show modal if there are unsaved changes', () => {
|
test('should block navigation and show modal if there are unsaved changes', async () => {
|
||||||
const { result } = renderHook(
|
const { result } = await setup();
|
||||||
() =>
|
|
||||||
useUnsavedChangesPrompt({
|
|
||||||
hasUnsavedChanges: true,
|
|
||||||
onSave: jest.fn(),
|
|
||||||
}),
|
|
||||||
{ wrapper },
|
|
||||||
);
|
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
history.push('/another-page');
|
result.current.router.history.push('/another-page');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.showModal).toBe(true);
|
await waitFor(() => expect(result.current.prompt.showModal).toBe(true));
|
||||||
|
expect(result.current.router.state.location.pathname).toBe('/dashboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should trigger onSave and hide modal on handleSaveAndCloseModal', async () => {
|
test('should trigger onSave and hide modal on handleSaveAndCloseModal', async () => {
|
||||||
const onSave = jest.fn().mockResolvedValue(undefined);
|
const onSave = jest.fn().mockResolvedValue(undefined);
|
||||||
|
const { result } = await setup({ onSave });
|
||||||
|
|
||||||
const { result } = renderHook(
|
await act(async () => {
|
||||||
() =>
|
await result.current.prompt.handleSaveAndCloseModal();
|
||||||
useUnsavedChangesPrompt({
|
});
|
||||||
hasUnsavedChanges: true,
|
|
||||||
onSave,
|
|
||||||
}),
|
|
||||||
{ wrapper },
|
|
||||||
);
|
|
||||||
|
|
||||||
await result.current.handleSaveAndCloseModal();
|
|
||||||
|
|
||||||
expect(onSave).toHaveBeenCalled();
|
expect(onSave).toHaveBeenCalled();
|
||||||
expect(result.current.showModal).toBe(false);
|
expect(result.current.prompt.showModal).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should trigger manual save and not show modal again', async () => {
|
test('should trigger manual save and not show modal again', async () => {
|
||||||
const onSave = jest.fn().mockResolvedValue(undefined);
|
const onSave = jest.fn().mockResolvedValue(undefined);
|
||||||
|
const { result } = await setup({ onSave });
|
||||||
|
|
||||||
const { result } = renderHook(
|
act(() => {
|
||||||
() =>
|
result.current.prompt.triggerManualSave();
|
||||||
useUnsavedChangesPrompt({
|
});
|
||||||
hasUnsavedChanges: true,
|
|
||||||
onSave,
|
|
||||||
}),
|
|
||||||
{ wrapper },
|
|
||||||
);
|
|
||||||
|
|
||||||
result.current.triggerManualSave();
|
|
||||||
|
|
||||||
expect(onSave).toHaveBeenCalled();
|
expect(onSave).toHaveBeenCalled();
|
||||||
expect(result.current.showModal).toBe(false);
|
expect(result.current.prompt.showModal).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should close modal when handleConfirmNavigation is called', () => {
|
test('should close modal when handleConfirmNavigation is called', async () => {
|
||||||
const onSave = jest.fn();
|
const { result } = await setup();
|
||||||
|
|
||||||
const { result } = renderHook(
|
|
||||||
() =>
|
|
||||||
useUnsavedChangesPrompt({
|
|
||||||
hasUnsavedChanges: true,
|
|
||||||
onSave,
|
|
||||||
}),
|
|
||||||
{ wrapper },
|
|
||||||
);
|
|
||||||
|
|
||||||
// First, trigger navigation to show the modal
|
// First, trigger navigation to show the modal
|
||||||
act(() => {
|
await act(async () => {
|
||||||
history.push('/another-page');
|
result.current.router.history.push('/another-page');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.showModal).toBe(true);
|
await waitFor(() => expect(result.current.prompt.showModal).toBe(true));
|
||||||
|
|
||||||
// Then call handleConfirmNavigation to discard changes
|
// Then call handleConfirmNavigation to discard changes
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.handleConfirmNavigation();
|
result.current.prompt.handleConfirmNavigation();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.showModal).toBe(false);
|
expect(result.current.prompt.showModal).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should preserve pathname, search, and state when confirming navigation', () => {
|
test('should preserve pathname, search, and state when confirming navigation', async () => {
|
||||||
const onSave = jest.fn();
|
const { result } = await setup();
|
||||||
const history = createMemoryHistory();
|
|
||||||
const wrapper = ({ children }: any) => (
|
|
||||||
<Router history={history}>{children}</Router>
|
|
||||||
);
|
|
||||||
|
|
||||||
const locationState = { fromDashboard: true, dashboardId: 123 };
|
const locationState = { fromDashboard: true, dashboardId: 123 };
|
||||||
const pathname = '/another-page';
|
const pathname = '/another-page';
|
||||||
const search = '?slice_id=42&foo=bar';
|
const search = '?slice_id=42&foo=bar';
|
||||||
|
|
||||||
const { result } = renderHook(
|
// Simulate a blocked navigation (the hook sets up a blocker internally)
|
||||||
() => useUnsavedChangesPrompt({ hasUnsavedChanges: true, onSave }),
|
await act(async () => {
|
||||||
{ wrapper },
|
result.current.router.history.push(`${pathname}${search}`, locationState);
|
||||||
);
|
|
||||||
|
|
||||||
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
|
// Modal should now be visible
|
||||||
expect(result.current.showModal).toBe(true);
|
await waitFor(() => expect(result.current.prompt.showModal).toBe(true));
|
||||||
|
|
||||||
// Confirm navigation
|
// Confirm navigation
|
||||||
act(() => {
|
await act(async () => {
|
||||||
result.current.handleConfirmNavigation();
|
result.current.prompt.handleConfirmNavigation();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Modal should close
|
// Modal should close
|
||||||
expect(result.current.showModal).toBe(false);
|
expect(result.current.prompt.showModal).toBe(false);
|
||||||
|
|
||||||
// Verify correct call with pathname, search, and state preserved
|
// Verify the blocked navigation resumed with pathname, search, and state
|
||||||
expect(pushSpy).toHaveBeenCalledWith({ pathname, search }, locationState);
|
await waitFor(() =>
|
||||||
|
expect(result.current.router.state.location.pathname).toBe(pathname),
|
||||||
pushSpy.mockRestore();
|
);
|
||||||
|
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');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ import {
|
|||||||
createStore,
|
createStore,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import AlertListComponent from 'src/pages/AlertReportList';
|
import AlertListComponent from 'src/pages/AlertReportList';
|
||||||
|
|
||||||
jest.setTimeout(30000);
|
jest.setTimeout(30000);
|
||||||
@@ -153,11 +153,11 @@ const renderAlertList = (props: Record<string, any> = {}) => {
|
|||||||
const store = createStore();
|
const store = createStore();
|
||||||
return render(
|
return render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<MemoryRouter>
|
<StandaloneRouter>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<AlertList user={mockUser} {...props} />
|
<AlertList user={mockUser} {...props} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>
|
</StandaloneRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -451,11 +451,11 @@ test('read-only users do not see delete and bulk select controls', async () => {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<MemoryRouter>
|
<StandaloneRouter>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<AlertList user={readOnlyUser} />
|
<AlertList user={readOnlyUser} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>
|
</StandaloneRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useMemo, useEffect, useCallback } from 'react';
|
import { useState, useMemo, useEffect, useCallback } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import {
|
import {
|
||||||
SupersetClient,
|
SupersetClient,
|
||||||
@@ -376,11 +376,13 @@ function AlertList({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Cell: ({ row: { original } }: any) => {
|
Cell: ({ row: { original } }: any) => {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
const handleEdit = () => handleAlertEdit(original);
|
const handleEdit = () => handleAlertEdit(original);
|
||||||
const handleDelete = () => setCurrentAlertDeleting(original);
|
const handleDelete = () => setCurrentAlertDeleting(original);
|
||||||
const handleGotoExecutionLog = () =>
|
const handleGotoExecutionLog = () =>
|
||||||
history.push(`/${original.type.toLowerCase()}/${original.id}/log`);
|
navigate({
|
||||||
|
to: `/${original.type.toLowerCase()}/${original.id}/log`,
|
||||||
|
});
|
||||||
|
|
||||||
const allowEdit =
|
const allowEdit =
|
||||||
original.owners.map((o: Owner) => o.id).includes(user.userId) ||
|
original.owners.map((o: Owner) => o.id).includes(user.userId) ||
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import {
|
|||||||
fireEvent,
|
fireEvent,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AnnotationLayersListComponent from 'src/pages/AnnotationLayerList';
|
import AnnotationLayersListComponent from 'src/pages/AnnotationLayerList';
|
||||||
@@ -81,11 +81,11 @@ fetchMock.get(layersRelatedEndpoint, {
|
|||||||
|
|
||||||
const renderAnnotationLayersList = (props = {}) =>
|
const renderAnnotationLayersList = (props = {}) =>
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<StandaloneRouter>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<AnnotationLayersList user={mockUser} {...props} />
|
<AnnotationLayersList user={mockUser} {...props} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>,
|
</StandaloneRouter>,
|
||||||
{
|
{
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store,
|
store,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { useMemo, useState } from 'react';
|
|||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useRouter } from '@tanstack/react-router';
|
||||||
import { useListViewResource } from 'src/views/CRUD/hooks';
|
import { useListViewResource } from 'src/views/CRUD/hooks';
|
||||||
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
||||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||||
@@ -140,16 +140,10 @@ function AnnotationLayersList({
|
|||||||
original: { id, name },
|
original: { id, name },
|
||||||
},
|
},
|
||||||
}: any) => {
|
}: any) => {
|
||||||
let hasHistory = true;
|
// If no router context exists, we know not to use <Link> in render
|
||||||
|
const hasRouter = Boolean(useRouter({ warn: false }));
|
||||||
|
|
||||||
try {
|
if (hasRouter) {
|
||||||
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>;
|
return <Link to={`/annotationlayer/${id}/annotation`}>{name}</Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||||
import { useParams, Link, useHistory } from 'react-router-dom';
|
import { useParams, Link, useRouter } from '@tanstack/react-router';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { SupersetClient, getClientErrorObject } from '@superset-ui/core';
|
import { SupersetClient, getClientErrorObject } from '@superset-ui/core';
|
||||||
import { css, styled } from '@apache-superset/core/theme';
|
import { css, styled } from '@apache-superset/core/theme';
|
||||||
@@ -68,7 +68,7 @@ function AnnotationList({
|
|||||||
addDangerToast,
|
addDangerToast,
|
||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
}: AnnotationListProps) {
|
}: AnnotationListProps) {
|
||||||
const { annotationLayerId }: any = useParams();
|
const { annotationLayerId }: any = useParams({ strict: false });
|
||||||
const {
|
const {
|
||||||
state: {
|
state: {
|
||||||
loading,
|
loading,
|
||||||
@@ -247,14 +247,8 @@ function AnnotationList({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let hasHistory = true;
|
// If no router context exists, we know not to use <Link> in render
|
||||||
|
const hasRouter = Boolean(useRouter({ warn: false }));
|
||||||
try {
|
|
||||||
useHistory();
|
|
||||||
} catch (err) {
|
|
||||||
// If error is thrown, we know not to use <Link> in render
|
|
||||||
hasHistory = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyState = {
|
const emptyState = {
|
||||||
title: t('No annotation yet'),
|
title: t('No annotation yet'),
|
||||||
@@ -277,7 +271,7 @@ function AnnotationList({
|
|||||||
<StyledHeader>
|
<StyledHeader>
|
||||||
<span>{t('Annotation Layer %s', annotationLayerName)}</span>
|
<span>{t('Annotation Layer %s', annotationLayerName)}</span>
|
||||||
<span>
|
<span>
|
||||||
{hasHistory ? (
|
{hasRouter ? (
|
||||||
<Link to="/annotationlayer/list/">{t('Back to all')}</Link>
|
<Link to="/annotationlayer/list/">{t('Back to all')}</Link>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Link href="/annotationlayer/list/">
|
<Typography.Link href="/annotationlayer/list/">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
import {
|
import {
|
||||||
render,
|
render,
|
||||||
waitFor,
|
waitFor,
|
||||||
@@ -261,11 +261,9 @@ describe('ChartPage', () => {
|
|||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to="/"
|
||||||
pathname: '/',
|
search={{ [URL_PARAMS.dashboardPageId.name]: dashboardPageId }}
|
||||||
search: `?${URL_PARAMS.dashboardPageId.name}=${dashboardPageId}`,
|
state={{ saveAction: 'overwrite' }}
|
||||||
state: { saveAction: 'overwrite' },
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Change route
|
Change route
|
||||||
</Link>
|
</Link>
|
||||||
@@ -319,7 +317,9 @@ describe('ChartPage', () => {
|
|||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
<>
|
<>
|
||||||
<Link to="/?slice_id=99">Navigate away</Link>
|
<Link to="/" search={{ slice_id: '99' }}>
|
||||||
|
Navigate away
|
||||||
|
</Link>
|
||||||
<ChartPage />
|
<ChartPage />
|
||||||
</>,
|
</>,
|
||||||
{
|
{
|
||||||
@@ -410,7 +410,9 @@ describe('ChartPage', () => {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<>
|
<>
|
||||||
<Link to="/?slice_id=99">Navigate</Link>
|
<Link to="/" search={{ slice_id: '99' }}>
|
||||||
|
Navigate
|
||||||
|
</Link>
|
||||||
<ChartPage />
|
<ChartPage />
|
||||||
</>,
|
</>,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useRouter, type HistoryLocation } from '@tanstack/react-router';
|
||||||
import type { Location, Action } from 'history';
|
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import {
|
import {
|
||||||
getLabelsColorMap,
|
getLabelsColorMap,
|
||||||
@@ -136,7 +135,7 @@ export default function ExplorePage() {
|
|||||||
const fetchGeneration = useRef(0);
|
const fetchGeneration = useRef(0);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
|
|
||||||
const loadExploreData = useCallback(
|
const loadExploreData = useCallback(
|
||||||
(
|
(
|
||||||
@@ -291,29 +290,36 @@ export default function ExplorePage() {
|
|||||||
|
|
||||||
// Initial fetch on mount
|
// Initial fetch on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadExploreData(history.location);
|
loadExploreData(router.history.location);
|
||||||
getLabelsColorMap().source = LabelsColorMapSource.Explore;
|
getLabelsColorMap().source = LabelsColorMapSource.Explore;
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Re-fetch on navigation or post-save.
|
// Re-fetch on navigation or post-save.
|
||||||
// PUSH/POP: full reload (unmount + re-fetch).
|
// PUSH/BACK/FORWARD/GO: full reload (unmount + re-fetch).
|
||||||
// REPLACE with saveAction state: re-fetch without unmount (keeps chart visible).
|
// REPLACE with saveAction state: re-fetch without unmount (keeps chart visible).
|
||||||
// Other REPLACE: ignored (URL sync from updateHistory).
|
// Other REPLACE: ignored (URL sync from updateHistory).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = history.listen((loc: Location, action: Action) => {
|
const unsubscribe = router.history.subscribe(
|
||||||
const saveAction = (loc.state as Record<string, unknown>)?.saveAction as
|
({
|
||||||
| SaveActionType
|
location: loc,
|
||||||
| undefined;
|
action,
|
||||||
if (action === 'PUSH' || action === 'POP') {
|
}: {
|
||||||
setIsLoaded(false);
|
location: HistoryLocation;
|
||||||
loadExploreData(loc, saveAction);
|
action: { type: string };
|
||||||
} else if (saveAction) {
|
}) => {
|
||||||
loadExploreData(loc, saveAction);
|
const saveAction = (loc.state as Record<string, unknown>)
|
||||||
}
|
?.saveAction as SaveActionType | undefined;
|
||||||
});
|
if (action.type !== 'REPLACE') {
|
||||||
return unlisten;
|
setIsLoaded(false);
|
||||||
}, [history, loadExploreData]);
|
loadExploreData(loc, saveAction);
|
||||||
|
} else if (saveAction) {
|
||||||
|
loadExploreData(loc, saveAction);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return unsubscribe;
|
||||||
|
}, [router, loadExploreData]);
|
||||||
|
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { styled } from '@apache-superset/core/theme';
|
|||||||
import { withTheme, Theme } from '@emotion/react';
|
import { withTheme, Theme } from '@emotion/react';
|
||||||
import { getUrlParam } from 'src/utils/urlUtils';
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
import { FilterPlugins, URL_PARAMS } from 'src/constants';
|
import { FilterPlugins, URL_PARAMS } from 'src/constants';
|
||||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
import { Link, useRouter } from '@tanstack/react-router';
|
||||||
import {
|
import {
|
||||||
AsyncSelect,
|
AsyncSelect,
|
||||||
Button,
|
Button,
|
||||||
@@ -49,10 +49,11 @@ import {
|
|||||||
datasetLabelLower,
|
datasetLabelLower,
|
||||||
} from 'src/features/semanticLayers/label';
|
} from 'src/features/semanticLayers/label';
|
||||||
|
|
||||||
export interface ChartCreationProps extends RouteComponentProps {
|
export interface ChartCreationProps {
|
||||||
user: UserWithPermissionsAndRoles;
|
user: UserWithPermissionsAndRoles;
|
||||||
addSuccessToast: (arg: string) => void;
|
addSuccessToast: (arg: string) => void;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
|
history: { push: (path: string) => void };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChartCreationState = {
|
export type ChartCreationState = {
|
||||||
@@ -397,4 +398,11 @@ export class ChartCreation extends PureComponent<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(withToasts(withTheme(ChartCreation)));
|
const ChartCreationWithToastsAndTheme = withToasts(withTheme(ChartCreation));
|
||||||
|
|
||||||
|
export default function ChartCreationPage(props: {
|
||||||
|
user: UserWithPermissionsAndRoles;
|
||||||
|
}) {
|
||||||
|
const { history } = useRouter();
|
||||||
|
return <ChartCreationWithToastsAndTheme {...props} history={history} />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import { isFeatureEnabled } from '@superset-ui/core';
|
import { isFeatureEnabled } from '@superset-ui/core';
|
||||||
import ChartList from 'src/pages/ChartList';
|
import ChartList from 'src/pages/ChartList';
|
||||||
import { API_ENDPOINTS, mockCharts, setupMocks } from './ChartList.testHelpers';
|
import { API_ENDPOINTS, mockCharts, setupMocks } from './ChartList.testHelpers';
|
||||||
@@ -116,11 +116,11 @@ const renderChartList = (
|
|||||||
|
|
||||||
return render(
|
return render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<MemoryRouter>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<ChartList user={user} {...props} />
|
<ChartList user={user} {...props} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>
|
</StandaloneRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { render } from 'spec/helpers/testing-library';
|
import { render } from 'spec/helpers/testing-library';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import ChartList from 'src/pages/ChartList';
|
import ChartList from 'src/pages/ChartList';
|
||||||
import handleResourceExport from 'src/utils/export';
|
import handleResourceExport from 'src/utils/export';
|
||||||
|
|
||||||
@@ -267,13 +267,15 @@ export const renderChartList = (user: any, props = {}, storeState = {}) => {
|
|||||||
|
|
||||||
const store = createMockStore(storeStateWithUser);
|
const store = createMockStore(storeStateWithUser);
|
||||||
|
|
||||||
|
// Browser (jsdom) history, not memory history: tests assert on
|
||||||
|
// window.location after navigation, matching the old BrowserRouter.
|
||||||
return render(
|
return render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<StandaloneRouter>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<ChartList user={user} {...props} />
|
<ChartList user={user} {...props} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</BrowserRouter>
|
</StandaloneRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ import {
|
|||||||
type ListViewFilter,
|
type ListViewFilter,
|
||||||
} from 'src/components';
|
} from 'src/components';
|
||||||
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
|
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { parseSearch } from 'src/router/searchParams';
|
||||||
import { dangerouslyGetItemDoNotUse } from 'src/utils/localStorageHelpers';
|
import { dangerouslyGetItemDoNotUse } from 'src/utils/localStorageHelpers';
|
||||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||||
@@ -175,7 +176,7 @@ function ChartList(props: ChartListProps) {
|
|||||||
user: { userId },
|
user: { userId },
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: {
|
state: {
|
||||||
@@ -365,22 +366,31 @@ function ChartList(props: ChartListProps) {
|
|||||||
description,
|
description,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}: any) => (
|
}: any) => {
|
||||||
<FlexRowContainer>
|
// url comes from the backend and may contain a query string
|
||||||
<Link to={url} data-test={`${sliceName}-list-chart-title`}>
|
// (cast: search prop types collapse to never for dynamic 'to' strings)
|
||||||
{certifiedBy && (
|
const [pathname, queryString] = url.split('?');
|
||||||
<>
|
return (
|
||||||
<CertifiedBadge
|
<FlexRowContainer>
|
||||||
certifiedBy={certifiedBy}
|
<Link
|
||||||
details={certificationDetails}
|
to={pathname}
|
||||||
/>{' '}
|
search={parseSearch(queryString ?? '') as never}
|
||||||
</>
|
data-test={`${sliceName}-list-chart-title`}
|
||||||
)}
|
>
|
||||||
{sliceName}
|
{certifiedBy && (
|
||||||
</Link>
|
<>
|
||||||
{description && <InfoTooltip tooltip={description} />}
|
<CertifiedBadge
|
||||||
</FlexRowContainer>
|
certifiedBy={certifiedBy}
|
||||||
),
|
details={certificationDetails}
|
||||||
|
/>{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{sliceName}
|
||||||
|
</Link>
|
||||||
|
{description && <InfoTooltip tooltip={description} />}
|
||||||
|
</FlexRowContainer>
|
||||||
|
);
|
||||||
|
},
|
||||||
Header: t('Name'),
|
Header: t('Name'),
|
||||||
accessor: 'slice_name',
|
accessor: 'slice_name',
|
||||||
id: 'slice_name',
|
id: 'slice_name',
|
||||||
@@ -857,7 +867,7 @@ function ChartList(props: ChartListProps) {
|
|||||||
name: t('Chart'),
|
name: t('Chart'),
|
||||||
buttonStyle: 'primary',
|
buttonStyle: 'primary',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
history.push('/chart/add');
|
navigate({ to: '/chart/add' });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import {
|
|||||||
fireEvent,
|
fireEvent,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import CssTemplatesListComponent from 'src/pages/CssTemplateList';
|
import CssTemplatesListComponent from 'src/pages/CssTemplateList';
|
||||||
@@ -81,11 +81,11 @@ fetchMock.get(templatesRelatedEndpoint, {
|
|||||||
|
|
||||||
const renderCssTemplatesList = (props = {}) =>
|
const renderCssTemplatesList = (props = {}) =>
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<StandaloneRouter>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<CssTemplatesList user={mockUser} {...props} />
|
<CssTemplatesList user={mockUser} {...props} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>,
|
</StandaloneRouter>,
|
||||||
{
|
{
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store,
|
store,
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from '@tanstack/react-router';
|
||||||
import { DashboardPage } from 'src/dashboard/containers/DashboardPage';
|
import { DashboardPage } from 'src/dashboard/containers/DashboardPage';
|
||||||
|
|
||||||
const DashboardRoute: FC = () => {
|
const DashboardRoute: FC = () => {
|
||||||
const { idOrSlug } = useParams<{ idOrSlug: string }>();
|
const { idOrSlug } = useParams({ strict: false }) as { idOrSlug: string };
|
||||||
return <DashboardPage idOrSlug={idOrSlug} />;
|
return <DashboardPage idOrSlug={idOrSlug} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import { isFeatureEnabled } from '@superset-ui/core';
|
import { isFeatureEnabled } from '@superset-ui/core';
|
||||||
import DashboardListComponent from 'src/pages/DashboardList';
|
import DashboardListComponent from 'src/pages/DashboardList';
|
||||||
import {
|
import {
|
||||||
@@ -123,11 +123,11 @@ const renderDashboardListWithPermissions = (
|
|||||||
|
|
||||||
return render(
|
return render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<MemoryRouter>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<DashboardList user={user} {...props} />
|
<DashboardList user={user} {...props} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>
|
</StandaloneRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import fetchMock from 'fetch-mock';
|
|||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import { render, screen } from 'spec/helpers/testing-library';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import DashboardListComponent from 'src/pages/DashboardList';
|
import DashboardListComponent from 'src/pages/DashboardList';
|
||||||
import handleResourceExport from 'src/utils/export';
|
import handleResourceExport from 'src/utils/export';
|
||||||
|
|
||||||
@@ -260,11 +260,11 @@ export const renderDashboardList = (
|
|||||||
|
|
||||||
return render(
|
return render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<MemoryRouter>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<DashboardList user={user} {...props} />
|
<DashboardList user={user} {...props} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>
|
</StandaloneRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import {
|
|||||||
import { styled } from '@apache-superset/core/theme';
|
import { styled } from '@apache-superset/core/theme';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useState, useMemo, useCallback } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
|
import { parseSearch } from 'src/router/searchParams';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import {
|
import {
|
||||||
createFetchRelated,
|
createFetchRelated,
|
||||||
@@ -343,19 +344,28 @@ function DashboardList(props: DashboardListProps) {
|
|||||||
certification_details: certificationDetails,
|
certification_details: certificationDetails,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}: any) => (
|
}: any) => {
|
||||||
<Link to={url} title={dashboardTitle}>
|
// url comes from the backend and may contain a query string
|
||||||
{certifiedBy && (
|
// (cast: search prop types collapse to never for dynamic 'to' strings)
|
||||||
<>
|
const [pathname, queryString] = url.split('?');
|
||||||
<CertifiedBadge
|
return (
|
||||||
certifiedBy={certifiedBy}
|
<Link
|
||||||
details={certificationDetails}
|
to={pathname}
|
||||||
/>{' '}
|
search={parseSearch(queryString ?? '') as never}
|
||||||
</>
|
title={dashboardTitle}
|
||||||
)}
|
>
|
||||||
{dashboardTitle}
|
{certifiedBy && (
|
||||||
</Link>
|
<>
|
||||||
),
|
<CertifiedBadge
|
||||||
|
certifiedBy={certifiedBy}
|
||||||
|
details={certificationDetails}
|
||||||
|
/>{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{dashboardTitle}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
Header: t('Name'),
|
Header: t('Name'),
|
||||||
accessor: 'dashboard_title',
|
accessor: 'dashboard_title',
|
||||||
id: 'dashboard_title',
|
id: 'dashboard_title',
|
||||||
|
|||||||
@@ -19,12 +19,10 @@
|
|||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import { render, screen } from 'spec/helpers/testing-library';
|
||||||
import AddDataset from 'src/pages/DatasetCreation';
|
import AddDataset from 'src/pages/DatasetCreation';
|
||||||
|
|
||||||
const mockHistoryPush = jest.fn();
|
const mockNavigate = jest.fn();
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('@tanstack/react-router', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('@tanstack/react-router'),
|
||||||
useHistory: () => ({
|
useNavigate: () => mockNavigate,
|
||||||
push: mockHistoryPush,
|
|
||||||
}),
|
|
||||||
useParams: () => ({ datasetId: undefined }),
|
useParams: () => ({ datasetId: undefined }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useReducer, Reducer, useEffect, useState } from 'react';
|
import { useReducer, Reducer, useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from '@tanstack/react-router';
|
||||||
import useDatasetsList from 'src/features/datasets/hooks/useDatasetLists';
|
import useDatasetsList from 'src/features/datasets/hooks/useDatasetLists';
|
||||||
import Header from 'src/features/datasets/AddDataset/Header';
|
import Header from 'src/features/datasets/AddDataset/Header';
|
||||||
import EditPage from 'src/features/datasets/AddDataset/EditDataset';
|
import EditPage from 'src/features/datasets/AddDataset/EditDataset';
|
||||||
@@ -95,7 +95,9 @@ export default function AddDataset() {
|
|||||||
dataset?.schema,
|
dataset?.schema,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { datasetId: id } = useParams<{ datasetId: string }>();
|
const { datasetId: id } = useParams({ strict: false }) as {
|
||||||
|
datasetId: string;
|
||||||
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!Number.isNaN(parseInt(id, 10))) {
|
if (!Number.isNaN(parseInt(id, 10))) {
|
||||||
setEditPageIsVisible(true);
|
setEditPageIsVisible(true);
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import { render, screen } from 'spec/helpers/testing-library';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import DatasetList from 'src/pages/DatasetList';
|
import DatasetList from 'src/pages/DatasetList';
|
||||||
import handleResourceExport from 'src/utils/export';
|
import handleResourceExport from 'src/utils/export';
|
||||||
|
|
||||||
@@ -386,11 +386,11 @@ export const renderDatasetList = (
|
|||||||
|
|
||||||
return render(
|
return render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<MemoryRouter>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<DatasetList user={user} {...props} />
|
<DatasetList user={user} {...props} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>
|
</StandaloneRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ import {
|
|||||||
Key,
|
Key,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { CellProps } from 'react-table';
|
import type { CellProps } from 'react-table';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { parseSearch } from 'src/router/searchParams';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import {
|
import {
|
||||||
createFetchRelated,
|
createFetchRelated,
|
||||||
@@ -200,7 +201,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
user,
|
user,
|
||||||
}) => {
|
}) => {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {
|
const {
|
||||||
state: { bulkSelectEnabled },
|
state: { bulkSelectEnabled },
|
||||||
@@ -674,8 +675,14 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
}: CellProps<Dataset>) => {
|
}: CellProps<Dataset>) => {
|
||||||
let titleLink: JSX.Element;
|
let titleLink: JSX.Element;
|
||||||
if (PREVENT_UNSAFE_DEFAULT_URLS_ON_DATASET) {
|
if (PREVENT_UNSAFE_DEFAULT_URLS_ON_DATASET) {
|
||||||
|
// explore_url comes from the backend and may contain a query string
|
||||||
|
const [explorePath, exploreQuery] = exploreURL.split('?');
|
||||||
titleLink = (
|
titleLink = (
|
||||||
<Link data-test="internal-link" to={exploreURL}>
|
<Link
|
||||||
|
data-test="internal-link"
|
||||||
|
to={explorePath}
|
||||||
|
search={parseSearch(exploreQuery ?? '')}
|
||||||
|
>
|
||||||
{datasetTitle}
|
{datasetTitle}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -1181,7 +1188,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
{
|
{
|
||||||
key: 'dataset',
|
key: 'dataset',
|
||||||
label: t('Dataset'),
|
label: t('Dataset'),
|
||||||
onClick: () => history.push('/dataset/add/'),
|
onClick: () => navigate({ to: '/dataset/add/' }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'semantic-view',
|
key: 'semantic-view',
|
||||||
@@ -1214,7 +1221,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
icon: <Icons.PlusOutlined iconSize="m" />,
|
icon: <Icons.PlusOutlined iconSize="m" />,
|
||||||
name: datasetLabel(),
|
name: datasetLabel(),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
history.push('/dataset/add/');
|
navigate({ to: '/dataset/add/' });
|
||||||
},
|
},
|
||||||
buttonStyle: 'primary',
|
buttonStyle: 'primary',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ fetchMock.get(reportEndpoint, {
|
|||||||
result: { name: 'Test 0' },
|
result: { name: 'Test 0' },
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('@tanstack/react-router', () => ({
|
||||||
...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
|
...jest.requireActual('@tanstack/react-router'), // use actual for all non-hook parts
|
||||||
useParams: () => ({ alertId: '1' }),
|
useParams: () => ({ alertId: '1' }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
fDuration,
|
fDuration,
|
||||||
} from '@superset-ui/core/utils/dates';
|
} from '@superset-ui/core/utils/dates';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from '@tanstack/react-router';
|
||||||
import { Label, Tooltip } from '@superset-ui/core/components';
|
import { Label, Tooltip } from '@superset-ui/core/components';
|
||||||
import { ListView } from 'src/components';
|
import { ListView } from 'src/components';
|
||||||
import SubMenu from 'src/features/home/SubMenu';
|
import SubMenu from 'src/features/home/SubMenu';
|
||||||
@@ -65,7 +65,7 @@ function ExecutionLog({
|
|||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
isReportEnabled,
|
isReportEnabled,
|
||||||
}: ExecutionLogProps) {
|
}: ExecutionLogProps) {
|
||||||
const { alertId }: any = useParams();
|
const { alertId }: any = useParams({ strict: false });
|
||||||
const {
|
const {
|
||||||
state: { loading, resourceCount: logCount, resourceCollection: logs },
|
state: { loading, resourceCount: logCount, resourceCollection: logs },
|
||||||
fetchData,
|
fetchData,
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ import {
|
|||||||
userEvent,
|
userEvent,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { MemoryRouter, Route } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import FileHandler from './index';
|
import FileHandler from './index';
|
||||||
|
|
||||||
const mockAddDangerToast = jest.fn();
|
const mockAddDangerToast = jest.fn();
|
||||||
const mockAddSuccessToast = jest.fn();
|
const mockAddSuccessToast = jest.fn();
|
||||||
const mockHistoryPush = jest.fn();
|
const mockNavigate = jest.fn();
|
||||||
|
|
||||||
jest.setTimeout(60000);
|
jest.setTimeout(60000);
|
||||||
|
|
||||||
@@ -82,12 +82,10 @@ jest.mock('src/features/databases/UploadDataModel', () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock react-router-dom's useHistory
|
// Mock @tanstack/react-router's useNavigate
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('@tanstack/react-router', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('@tanstack/react-router'),
|
||||||
useHistory: () => ({
|
useNavigate: () => mockNavigate,
|
||||||
push: mockHistoryPush,
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the File API
|
// Mock the File API
|
||||||
@@ -189,11 +187,9 @@ afterEach(async () => {
|
|||||||
|
|
||||||
test('shows error when launchQueue is not supported', async () => {
|
test('shows error when launchQueue is not supported', async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -201,7 +197,7 @@ test('shows error when launchQueue is not supported', async () => {
|
|||||||
expect(mockAddDangerToast).toHaveBeenCalledWith(
|
expect(mockAddDangerToast).toHaveBeenCalledWith(
|
||||||
'File handling is not supported in this browser. Please use a modern browser like Chrome or Edge.',
|
'File handling is not supported in this browser. Please use a modern browser like Chrome or Edge.',
|
||||||
);
|
);
|
||||||
expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');
|
expect(mockNavigate).toHaveBeenCalledWith({ to: '/superset/welcome/' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,11 +205,9 @@ test('redirects when no files are provided', async () => {
|
|||||||
const { triggerConsumer } = setupLaunchQueue();
|
const { triggerConsumer } = setupLaunchQueue();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -221,7 +215,7 @@ test('redirects when no files are provided', async () => {
|
|||||||
await triggerConsumer({ files: [] });
|
await triggerConsumer({ files: [] });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');
|
expect(mockNavigate).toHaveBeenCalledWith({ to: '/superset/welcome/' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,11 +225,9 @@ test.skip('handles CSV file correctly', async () => {
|
|||||||
setupLaunchQueue(fileHandle);
|
setupLaunchQueue(fileHandle);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -252,11 +244,9 @@ test('handles Excel (.xls) file correctly', async () => {
|
|||||||
setupLaunchQueue(fileHandle);
|
setupLaunchQueue(fileHandle);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -271,11 +261,9 @@ test('handles Excel (.xlsx) file correctly', async () => {
|
|||||||
const { triggerConsumer } = setupLaunchQueue();
|
const { triggerConsumer } = setupLaunchQueue();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -292,11 +280,9 @@ test('handles Parquet file correctly', async () => {
|
|||||||
setupLaunchQueue(fileHandle);
|
setupLaunchQueue(fileHandle);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -310,11 +296,9 @@ test('shows error for unsupported file type', async () => {
|
|||||||
const { triggerConsumer } = setupLaunchQueue();
|
const { triggerConsumer } = setupLaunchQueue();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -326,7 +310,7 @@ test('shows error for unsupported file type', async () => {
|
|||||||
expect(mockAddDangerToast).toHaveBeenCalledWith(
|
expect(mockAddDangerToast).toHaveBeenCalledWith(
|
||||||
'Unsupported file type. Please use CSV, Excel, or Columnar files.',
|
'Unsupported file type. Please use CSV, Excel, or Columnar files.',
|
||||||
);
|
);
|
||||||
expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');
|
expect(mockNavigate).toHaveBeenCalledWith({ to: '/superset/welcome/' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -335,11 +319,9 @@ test('handles file with uppercase extension', async () => {
|
|||||||
setupLaunchQueue(fileHandle);
|
setupLaunchQueue(fileHandle);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -352,11 +334,9 @@ test('handles errors during file processing', async () => {
|
|||||||
const { triggerConsumer } = setupLaunchQueue();
|
const { triggerConsumer } = setupLaunchQueue();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -378,7 +358,7 @@ test('handles errors during file processing', async () => {
|
|||||||
expect(mockAddDangerToast).toHaveBeenCalledWith(
|
expect(mockAddDangerToast).toHaveBeenCalledWith(
|
||||||
'Failed to open file. Please try again.',
|
'Failed to open file. Please try again.',
|
||||||
);
|
);
|
||||||
expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');
|
expect(mockNavigate).toHaveBeenCalledWith({ to: '/superset/welcome/' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -387,11 +367,9 @@ test('modal close redirects to welcome page', async () => {
|
|||||||
setupLaunchQueue(fileHandle);
|
setupLaunchQueue(fileHandle);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -402,7 +380,7 @@ test('modal close redirects to welcome page', async () => {
|
|||||||
await userEvent.click(screen.getByRole('button', { name: 'Close' }));
|
await userEvent.click(screen.getByRole('button', { name: 'Close' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');
|
expect(mockNavigate).toHaveBeenCalledWith({ to: '/superset/welcome/' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -410,11 +388,9 @@ test('shows loading state while waiting for file', () => {
|
|||||||
setupLaunchQueue();
|
setupLaunchQueue();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/superset/file-handler']}>
|
<StandaloneRouter initialEntries={['/superset/file-handler']}>
|
||||||
<Route path="/superset/file-handler">
|
<FileHandler />
|
||||||
<FileHandler />
|
</StandaloneRouter>,
|
||||||
</Route>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { Loading } from '@superset-ui/core/components';
|
import { Loading } from '@superset-ui/core/components';
|
||||||
import UploadDataModal from 'src/features/databases/UploadDataModel';
|
import UploadDataModal from 'src/features/databases/UploadDataModel';
|
||||||
@@ -41,7 +41,7 @@ interface FileHandlerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FileHandler = ({ addDangerToast, addSuccessToast }: FileHandlerProps) => {
|
const FileHandler = ({ addDangerToast, addSuccessToast }: FileHandlerProps) => {
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||||
const [uploadType, setUploadType] = useState<
|
const [uploadType, setUploadType] = useState<
|
||||||
'csv' | 'excel' | 'columnar' | null
|
'csv' | 'excel' | 'columnar' | null
|
||||||
@@ -59,13 +59,13 @@ const FileHandler = ({ addDangerToast, addSuccessToast }: FileHandlerProps) => {
|
|||||||
'File handling is not supported in this browser. Please use a modern browser like Chrome or Edge.',
|
'File handling is not supported in this browser. Please use a modern browser like Chrome or Edge.',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
history.push('/superset/welcome/');
|
navigate({ to: '/superset/welcome/' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
launchQueue.setConsumer(async (launchParams: FileLaunchParams) => {
|
launchQueue.setConsumer(async (launchParams: FileLaunchParams) => {
|
||||||
if (!launchParams.files || launchParams.files.length === 0) {
|
if (!launchParams.files || launchParams.files.length === 0) {
|
||||||
history.push('/superset/welcome/');
|
navigate({ to: '/superset/welcome/' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ const FileHandler = ({ addDangerToast, addSuccessToast }: FileHandlerProps) => {
|
|||||||
'Unsupported file type. Please use CSV, Excel, or Columnar files.',
|
'Unsupported file type. Please use CSV, Excel, or Columnar files.',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
history.push('/superset/welcome/');
|
navigate({ to: '/superset/welcome/' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,19 +103,19 @@ const FileHandler = ({ addDangerToast, addSuccessToast }: FileHandlerProps) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling file launch:', error);
|
console.error('Error handling file launch:', error);
|
||||||
addDangerToast(t('Failed to open file. Please try again.'));
|
addDangerToast(t('Failed to open file. Please try again.'));
|
||||||
history.push('/superset/welcome/');
|
navigate({ to: '/superset/welcome/' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFileLaunch();
|
handleFileLaunch();
|
||||||
}, [history, addDangerToast]);
|
}, [navigate, addDangerToast]);
|
||||||
|
|
||||||
const handleModalClose = () => {
|
const handleModalClose = () => {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
setUploadFile(null);
|
setUploadFile(null);
|
||||||
setUploadType(null);
|
setUploadType(null);
|
||||||
history.push('/superset/welcome/');
|
navigate({ to: '/superset/welcome/' });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!uploadFile || !uploadType) {
|
if (!uploadFile || !uploadType) {
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ import {
|
|||||||
act,
|
act,
|
||||||
within,
|
within,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
|
||||||
import GroupsList from './index';
|
import GroupsList from './index';
|
||||||
|
|
||||||
const mockStore = configureStore([thunk]);
|
const mockStore = configureStore([thunk]);
|
||||||
@@ -78,14 +75,11 @@ jest.mock('src/dashboard/util/permissionUtils', () => ({
|
|||||||
describe('GroupsList', () => {
|
describe('GroupsList', () => {
|
||||||
const renderComponent = async () => {
|
const renderComponent = async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(
|
render(<GroupsList user={mockUser} />, {
|
||||||
<MemoryRouter>
|
useRedux: true,
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
store,
|
||||||
<GroupsList user={mockUser} />
|
useQueryParams: true,
|
||||||
</QueryParamProvider>
|
});
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true, store },
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useMemo, useState, useCallback, ReactElement, useEffect } from 'react';
|
import { useMemo, useState, useCallback, ReactElement, useEffect } from 'react';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { QueryState, SupersetClient } from '@superset-ui/core';
|
import { QueryState, SupersetClient } from '@superset-ui/core';
|
||||||
import { css, styled, useTheme } from '@apache-superset/core/theme';
|
import { css, styled, useTheme } from '@apache-superset/core/theme';
|
||||||
@@ -116,7 +117,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
|
|||||||
useState<QueryObject>();
|
useState<QueryObject>();
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
|
|
||||||
// Preload SQL language since this component will definitely display SQL
|
// Preload SQL language since this component will definitely display SQL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -400,7 +401,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
|
|||||||
},
|
},
|
||||||
}: any) => (
|
}: any) => (
|
||||||
<Tooltip title={t('Open query in SQL Lab')} placement="bottom">
|
<Tooltip title={t('Open query in SQL Lab')} placement="bottom">
|
||||||
<Link to={`/sqllab?queryId=${id}`}>
|
<Link to="/sqllab" search={{ queryId: String(id) }}>
|
||||||
<Icons.Full iconSize="l" />
|
<Icons.Full iconSize="l" />
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -493,7 +494,9 @@ function QueryList({ addDangerToast }: QueryListProps) {
|
|||||||
query={queryCurrentlyPreviewing}
|
query={queryCurrentlyPreviewing}
|
||||||
queries={queries}
|
queries={queries}
|
||||||
fetchData={handleQueryPreview}
|
fetchData={handleQueryPreview}
|
||||||
openInSqlLab={(id: number) => history.push(`/sqllab?queryId=${id}`)}
|
openInSqlLab={(id: number) =>
|
||||||
|
pushAppHref(router, `/sqllab?queryId=${id}`)
|
||||||
|
}
|
||||||
show
|
show
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import { render, screen } from 'spec/helpers/testing-library';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import Register from './index';
|
import Register from './index';
|
||||||
|
|
||||||
jest.mock('src/utils/getBootstrapData', () => ({
|
jest.mock('src/utils/getBootstrapData', () => ({
|
||||||
@@ -38,9 +38,9 @@ jest.mock('react-google-recaptcha', () => ({
|
|||||||
|
|
||||||
const renderRegister = () =>
|
const renderRegister = () =>
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<StandaloneRouter>
|
||||||
<Register />
|
<Register />
|
||||||
</MemoryRouter>,
|
</StandaloneRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
test('should render register form elements', () => {
|
test('should render register form elements', () => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||||
import ReactCAPTCHA from 'react-google-recaptcha';
|
import ReactCAPTCHA from 'react-google-recaptcha';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from '@tanstack/react-router';
|
||||||
|
|
||||||
interface RegisterForm {
|
interface RegisterForm {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -68,7 +68,9 @@ export default function Login() {
|
|||||||
const [form] = Form.useForm<RegisterForm>();
|
const [form] = Form.useForm<RegisterForm>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [captchaResponse, setCaptchaResponse] = useState<string | null>(null);
|
const [captchaResponse, setCaptchaResponse] = useState<string | null>(null);
|
||||||
const { activationHash } = useParams<{ activationHash?: string }>();
|
const { activationHash } = useParams({ strict: false }) as {
|
||||||
|
activationHash?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const bootstrapData = getBootstrapData();
|
const bootstrapData = getBootstrapData();
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ import {
|
|||||||
act,
|
act,
|
||||||
within,
|
within,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
|
||||||
import RolesList from './index';
|
import RolesList from './index';
|
||||||
|
|
||||||
const mockStore = configureStore([thunk]);
|
const mockStore = configureStore([thunk]);
|
||||||
@@ -106,17 +103,13 @@ describe('RolesList', () => {
|
|||||||
const mounted = act(async () => {
|
const mounted = act(async () => {
|
||||||
const mockedProps = {};
|
const mockedProps = {};
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<RolesList
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
user={mockUser}
|
||||||
<RolesList
|
addDangerToast={() => {}}
|
||||||
user={mockUser}
|
addSuccessToast={() => {}}
|
||||||
addDangerToast={() => {}}
|
{...mockedProps}
|
||||||
addSuccessToast={() => {}}
|
/>,
|
||||||
{...mockedProps}
|
{ useRedux: true, store, useQueryParams: true },
|
||||||
/>
|
|
||||||
</QueryParamProvider>
|
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true, store },
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return mounted;
|
return mounted;
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
*/
|
*/
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { act, render, screen, within } from 'spec/helpers/testing-library';
|
import { act, render, screen, within } from 'spec/helpers/testing-library';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import RowLevelSecurityList from '.';
|
import RowLevelSecurityList from '.';
|
||||||
|
|
||||||
@@ -109,11 +109,11 @@ describe('RuleList RTL', () => {
|
|||||||
const mounted = act(async () => {
|
const mounted = act(async () => {
|
||||||
const mockedProps = {};
|
const mockedProps = {};
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<StandaloneRouter>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<RowLevelSecurityList {...mockedProps} user={mockUser} />
|
<RowLevelSecurityList {...mockedProps} user={mockUser} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>,
|
</StandaloneRouter>,
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,17 +25,22 @@ import {
|
|||||||
fireEvent,
|
fireEvent,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
import { useLocation } from '@tanstack/react-router';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import * as getBootstrapData from 'src/utils/getBootstrapData';
|
import * as getBootstrapData from 'src/utils/getBootstrapData';
|
||||||
import SavedQueryList from '.';
|
import SavedQueryList from '.';
|
||||||
|
|
||||||
// Renders the current router pathname+search so tests can assert navigation.
|
// Renders the current router pathname+search so tests can assert navigation.
|
||||||
function LocationDisplay() {
|
function LocationDisplay() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
// searchStr already includes the leading '?' (it is the stringifySearch
|
||||||
|
// output), matching react-router's location.search semantics.
|
||||||
return (
|
return (
|
||||||
<div data-test="location-display">{`${location.pathname}${location.search}`}</div>
|
<div data-test="location-display">
|
||||||
|
{`${location.pathname}${location.searchStr}`}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,12 +99,12 @@ fetchMock.delete(queryEndpoint, {}, { name: queryEndpoint });
|
|||||||
|
|
||||||
const renderList = (props = {}, storeOverrides = {}) =>
|
const renderList = (props = {}, storeOverrides = {}) =>
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<StandaloneRouter initialEntries={['/']}>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<SavedQueryList user={mockUser} {...props} />
|
<SavedQueryList user={mockUser} {...props} />
|
||||||
<LocationDisplay />
|
<LocationDisplay />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>,
|
</StandaloneRouter>,
|
||||||
{
|
{
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: configureStore([thunk])({
|
store: configureStore([thunk])({
|
||||||
@@ -256,11 +261,11 @@ describe('SavedQueryList', () => {
|
|||||||
test('"+ Query" button pushes a router-relative path (subdirectory deployment)', async () => {
|
test('"+ Query" button pushes a router-relative path (subdirectory deployment)', async () => {
|
||||||
// Simulate SUPERSET_APP_ROOT=/superset. ensureAppRoot/makeUrl read
|
// Simulate SUPERSET_APP_ROOT=/superset. ensureAppRoot/makeUrl read
|
||||||
// applicationRoot() dynamically, so mocking it here makes the buggy code
|
// applicationRoot() dynamically, so mocking it here makes the buggy code
|
||||||
// path (makeUrl() around history.push) produce '/superset/sqllab?new=true'
|
// path (makeUrl() around the history push) produce
|
||||||
// instead of being a no-op. React Router's <Router basename> prefixes the
|
// '/superset/sqllab?new=true' instead of being a no-op. The router's
|
||||||
// app root on its own, so history.push MUST receive a path without the
|
// basepath prefixes the app root on its own, so the push MUST receive a
|
||||||
// app-root prefix — otherwise navigation lands at /superset/superset/sqllab
|
// path without the app-root prefix — otherwise navigation lands at
|
||||||
// and shows a blank page (sc-103661).
|
// /superset/superset/sqllab and shows a blank page (sc-103661).
|
||||||
const applicationRootSpy = jest
|
const applicationRootSpy = jest
|
||||||
.spyOn(getBootstrapData, 'applicationRoot')
|
.spyOn(getBootstrapData, 'applicationRoot')
|
||||||
.mockReturnValue('/superset');
|
.mockReturnValue('/superset');
|
||||||
@@ -276,10 +281,11 @@ describe('SavedQueryList', () => {
|
|||||||
fireEvent.click(queryButton);
|
fireEvent.click(queryButton);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// The MemoryRouter in renderList uses the default ('/') basename, so
|
// The StandaloneRouter in renderList uses the default ('/') basepath,
|
||||||
// useLocation reflects exactly what history.push received. A correct
|
// so useLocation reflects exactly what the history push received. A
|
||||||
// router-relative push produces '/sqllab?new=true'; a buggy push that
|
// correct router-relative push produces '/sqllab?new=true'; a buggy
|
||||||
// re-applied the app root would produce '/superset/sqllab?new=true'.
|
// push that re-applied the app root would produce
|
||||||
|
// '/superset/sqllab?new=true'.
|
||||||
const location = screen.getByTestId('location-display').textContent;
|
const location = screen.getByTestId('location-display').textContent;
|
||||||
expect(location).toBe('/sqllab?new=true');
|
expect(location).toBe('/sqllab?new=true');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import {
|
|||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { styled } from '@apache-superset/core/theme';
|
import { styled } from '@apache-superset/core/theme';
|
||||||
import { useCallback, useMemo, useState, MouseEvent } from 'react';
|
import { useCallback, useMemo, useState, MouseEvent } from 'react';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useRouter } from '@tanstack/react-router';
|
||||||
|
import { pushAppHref } from 'src/router/navigation';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import {
|
import {
|
||||||
createErrorHandler,
|
createErrorHandler,
|
||||||
@@ -146,7 +147,7 @@ function SavedQueryList({
|
|||||||
sshTunnelPrivateKeyPasswordFields,
|
sshTunnelPrivateKeyPasswordFields,
|
||||||
setSSHTunnelPrivateKeyPasswordFields,
|
setSSHTunnelPrivateKeyPasswordFields,
|
||||||
] = useState<string[]>([]);
|
] = useState<string[]>([]);
|
||||||
const history = useHistory();
|
const router = useRouter();
|
||||||
|
|
||||||
const openSavedQueryImportModal = () => {
|
const openSavedQueryImportModal = () => {
|
||||||
showImportModal(true);
|
showImportModal(true);
|
||||||
@@ -223,9 +224,9 @@ function SavedQueryList({
|
|||||||
name: t('Query'),
|
name: t('Query'),
|
||||||
buttonStyle: 'primary',
|
buttonStyle: 'primary',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// React Router's basename already includes the application root; passing
|
// The router's basepath already includes the application root; passing
|
||||||
// a relative path ensures correct navigation under subdirectory deployments.
|
// a relative path ensures correct navigation under subdirectory deployments.
|
||||||
history.push('/sqllab?new=true');
|
pushAppHref(router, '/sqllab?new=true');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -247,9 +248,9 @@ function SavedQueryList({
|
|||||||
if (openInNewWindow) {
|
if (openInNewWindow) {
|
||||||
window.open(makeUrl(`/sqllab?savedQueryId=${id}`));
|
window.open(makeUrl(`/sqllab?savedQueryId=${id}`));
|
||||||
} else {
|
} else {
|
||||||
// React Router's basename already includes the application root; passing
|
// The router's basepath already includes the application root; passing
|
||||||
// a relative path ensures correct navigation under subdirectory deployments.
|
// a relative path ensures correct navigation under subdirectory deployments.
|
||||||
history.push(`/sqllab?savedQueryId=${id}`);
|
pushAppHref(router, `/sqllab?savedQueryId=${id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -342,7 +343,11 @@ function SavedQueryList({
|
|||||||
row: {
|
row: {
|
||||||
original: { id, label },
|
original: { id, label },
|
||||||
},
|
},
|
||||||
}: any) => <Link to={`/sqllab?savedQueryId=${id}`}>{label}</Link>,
|
}: any) => (
|
||||||
|
<Link to="/sqllab" search={{ savedQueryId: String(id) }}>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
id: 'label',
|
id: 'label',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
import { createContext, useContext, FC, ReactNode } from 'react';
|
import { createContext, useContext, FC, ReactNode } from 'react';
|
||||||
|
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from '@tanstack/react-router';
|
||||||
|
|
||||||
export type LocationState = {
|
export type LocationState = {
|
||||||
requestedQuery?: Record<string, any>;
|
requestedQuery?: Record<string, any>;
|
||||||
@@ -34,11 +34,14 @@ const EMPTY_STATE: LocationState = {};
|
|||||||
export const LocationProvider: FC<{ children?: ReactNode }> = ({
|
export const LocationProvider: FC<{ children?: ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation<LocationState>();
|
const location = useLocation();
|
||||||
if (location.state) {
|
// TanStack's location.state is always an object (it carries internal
|
||||||
return <Provider value={location.state}>{children}</Provider>;
|
// router keys), so check for the keys this context actually consumes.
|
||||||
|
const state = location.state as LocationState;
|
||||||
|
if ('requestedQuery' in state || 'isDataset' in state) {
|
||||||
|
return <Provider value={state}>{children}</Provider>;
|
||||||
}
|
}
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.searchStr);
|
||||||
const permalink = location.pathname.match(/\/p\/\w+/)?.[0].slice(3);
|
const permalink = location.pathname.match(/\/p\/\w+/)?.[0].slice(3);
|
||||||
if (queryParams.size > 0 || permalink) {
|
if (queryParams.size > 0 || permalink) {
|
||||||
const autorun = queryParams.get('autorun') === 'true';
|
const autorun = queryParams.get('autorun') === 'true';
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
|
|||||||
import { dangerouslyGetItemDoNotUse } from 'src/utils/localStorageHelpers';
|
import { dangerouslyGetItemDoNotUse } from 'src/utils/localStorageHelpers';
|
||||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||||
import { Icons } from '@superset-ui/core/components/Icons';
|
import { Icons } from '@superset-ui/core/components/Icons';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from '@tanstack/react-router';
|
||||||
import { deleteTags } from 'src/features/tags/tags';
|
import { deleteTags } from 'src/features/tags/tags';
|
||||||
import { QueryObjectColumns, Tag } from 'src/views/CRUD/types';
|
import { QueryObjectColumns, Tag } from 'src/views/CRUD/types';
|
||||||
import TagModal from 'src/features/tags/TagModal';
|
import TagModal from 'src/features/tags/TagModal';
|
||||||
@@ -168,7 +168,9 @@ function TagList(props: TagListProps) {
|
|||||||
},
|
},
|
||||||
}: any) => (
|
}: any) => (
|
||||||
<AntdTag>
|
<AntdTag>
|
||||||
<Link to={`/superset/all_entities/?id=${id}`}>{tagName}</Link>
|
<Link to="/superset/all_entities/" search={{ id: String(id) }}>
|
||||||
|
{tagName}
|
||||||
|
</Link>
|
||||||
</AntdTag>
|
</AntdTag>
|
||||||
),
|
),
|
||||||
Header: t('Name'),
|
Header: t('Name'),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { StandaloneRouter } from 'src/router/StandaloneRouter';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import {
|
import {
|
||||||
render,
|
render,
|
||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
fireEvent,
|
fireEvent,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
import { TanstackRouterAdapter } from 'src/router/queryParamAdapter';
|
||||||
import { TaskStatus, TaskScope } from 'src/features/tasks/types';
|
import { TaskStatus, TaskScope } from 'src/features/tasks/types';
|
||||||
import TaskList from 'src/pages/TaskList';
|
import TaskList from 'src/pages/TaskList';
|
||||||
|
|
||||||
@@ -189,11 +189,11 @@ fetchMock.post(
|
|||||||
|
|
||||||
const renderTaskList = (props = {}, userProp = mockUser) =>
|
const renderTaskList = (props = {}, userProp = mockUser) =>
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<StandaloneRouter>
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
<QueryParamProvider adapter={TanstackRouterAdapter}>
|
||||||
<TaskList {...props} user={userProp} />
|
<TaskList {...props} user={userProp} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</MemoryRouter>,
|
</StandaloneRouter>,
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ import {
|
|||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
|
||||||
import UserInfo from 'src/pages/UserInfo';
|
import UserInfo from 'src/pages/UserInfo';
|
||||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||||
|
|
||||||
@@ -64,14 +61,11 @@ const mockUser: UserWithPermissionsAndRoles = {
|
|||||||
describe('UserInfo', () => {
|
describe('UserInfo', () => {
|
||||||
const renderPage = async (user: UserWithPermissionsAndRoles = mockUser) =>
|
const renderPage = async (user: UserWithPermissionsAndRoles = mockUser) =>
|
||||||
act(async () => {
|
act(async () => {
|
||||||
render(
|
render(<UserInfo user={user} />, {
|
||||||
<MemoryRouter>
|
useRedux: true,
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
store,
|
||||||
<UserInfo user={user} />
|
useQueryParams: true,
|
||||||
</QueryParamProvider>
|
});
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true, store },
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ import {
|
|||||||
act,
|
act,
|
||||||
within,
|
within,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
|
||||||
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
|
|
||||||
import UsersList from './index';
|
import UsersList from './index';
|
||||||
|
|
||||||
const mockStore = configureStore([thunk]);
|
const mockStore = configureStore([thunk]);
|
||||||
@@ -99,14 +96,11 @@ describe('UsersList', () => {
|
|||||||
async function renderAndWait() {
|
async function renderAndWait() {
|
||||||
const mounted = act(async () => {
|
const mounted = act(async () => {
|
||||||
const mockedProps = {};
|
const mockedProps = {};
|
||||||
render(
|
render(<UsersList user={mockUser} {...mockedProps} />, {
|
||||||
<MemoryRouter>
|
useRedux: true,
|
||||||
<QueryParamProvider adapter={ReactRouter5Adapter}>
|
store,
|
||||||
<UsersList user={mockUser} {...mockedProps} />
|
useQueryParams: true,
|
||||||
</QueryParamProvider>
|
});
|
||||||
</MemoryRouter>,
|
|
||||||
{ useRedux: true, store },
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
return mounted;
|
return mounted;
|
||||||
}
|
}
|
||||||
|
|||||||
88
superset-frontend/src/router/StandaloneRouter.tsx
Normal file
88
superset-frontend/src/router/StandaloneRouter.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ReactNode, useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
createMemoryHistory,
|
||||||
|
createRootRoute,
|
||||||
|
createRouter,
|
||||||
|
RouterContextProvider,
|
||||||
|
type RouterHistory,
|
||||||
|
} from '@tanstack/react-router';
|
||||||
|
import { parseSearch, stringifySearch } from './searchParams';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A minimal router host for trees that need router context (Link,
|
||||||
|
* useNavigate, useLocation, useBlocker) without the SPA route table:
|
||||||
|
* the menu entry rendered on Flask-served pages, and component tests.
|
||||||
|
*
|
||||||
|
* Children are rendered directly inside RouterContextProvider (rather
|
||||||
|
* than through RouterProvider's async match rendering) so the tree
|
||||||
|
* mounts synchronously — required by tests that query right after
|
||||||
|
* render().
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface StandaloneRouterProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
basepath?: string;
|
||||||
|
/** When provided, an in-memory history is used (tests). */
|
||||||
|
initialEntries?: string[];
|
||||||
|
initialIndex?: number;
|
||||||
|
/** Explicit history instance (tests that assert on navigation). */
|
||||||
|
history?: RouterHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStandaloneRouter({
|
||||||
|
basepath,
|
||||||
|
initialEntries,
|
||||||
|
initialIndex,
|
||||||
|
history,
|
||||||
|
}: Omit<StandaloneRouterProps, 'children'> = {}) {
|
||||||
|
return createRouter({
|
||||||
|
routeTree: createRootRoute(),
|
||||||
|
basepath,
|
||||||
|
history:
|
||||||
|
history ??
|
||||||
|
(initialEntries
|
||||||
|
? createMemoryHistory({ initialEntries, initialIndex })
|
||||||
|
: undefined),
|
||||||
|
parseSearch,
|
||||||
|
stringifySearch,
|
||||||
|
trailingSlash: 'preserve',
|
||||||
|
defaultPreload: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StandaloneRouter({
|
||||||
|
children,
|
||||||
|
...routerOptions
|
||||||
|
}: StandaloneRouterProps) {
|
||||||
|
const [router] = useState(() => createStandaloneRouter(routerOptions));
|
||||||
|
|
||||||
|
// RouterProvider's internal Transitioner normally wires history changes
|
||||||
|
// to router state; replicate that minimal wiring since we render
|
||||||
|
// children directly instead of through RouterProvider. No initial
|
||||||
|
// load(): location state is already populated by createRouter, and the
|
||||||
|
// extra state flush would re-render consumers (which breaks tests that
|
||||||
|
// mock selectors with mockReturnValueOnce sequences).
|
||||||
|
useEffect(() => router.history.subscribe(router.load), [router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouterContextProvider router={router}>{children}</RouterContextProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
164
superset-frontend/src/router/index.tsx
Normal file
164
superset-frontend/src/router/index.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Suspense, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
createRootRoute,
|
||||||
|
createRoute,
|
||||||
|
createRouter,
|
||||||
|
Outlet,
|
||||||
|
useLocation,
|
||||||
|
} from '@tanstack/react-router';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { css } from '@apache-superset/core/theme';
|
||||||
|
import { Layout, Loading } from '@superset-ui/core/components';
|
||||||
|
import { ErrorBoundary } from 'src/components';
|
||||||
|
import Menu from 'src/features/home/Menu';
|
||||||
|
import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
|
||||||
|
import ToastContainer from 'src/components/MessageToasts/ToastContainer';
|
||||||
|
import { routes, isFrontendRoute } from 'src/views/routes';
|
||||||
|
import { Logger, LOG_ACTIONS_SPA_NAVIGATION } from 'src/logger/LogUtils';
|
||||||
|
import { logEvent } from 'src/logger/actions';
|
||||||
|
import { store } from 'src/views/store';
|
||||||
|
import ExtensionsStartup from 'src/extensions/ExtensionsStartup';
|
||||||
|
import { RootContextProviders } from 'src/views/RootContextProviders';
|
||||||
|
import { ScrollToTop } from 'src/views/ScrollToTop';
|
||||||
|
import { parseSearch, stringifySearch } from './searchParams';
|
||||||
|
|
||||||
|
const bootstrapData = getBootstrapData();
|
||||||
|
|
||||||
|
let lastLocationPathname: string;
|
||||||
|
|
||||||
|
const boundActions = bindActionCreators({ logEvent }, store.dispatch);
|
||||||
|
|
||||||
|
const LocationPathnameLogger = () => {
|
||||||
|
const pathname = useLocation({ select: location => location.pathname });
|
||||||
|
useEffect(() => {
|
||||||
|
// This will log client side route changes for single page app user navigation
|
||||||
|
boundActions.logEvent(LOG_ACTIONS_SPA_NAVIGATION, { path: pathname });
|
||||||
|
// reset performance logger timer start point to avoid soft navigation
|
||||||
|
// cause dashboard perf measurement problem
|
||||||
|
if (lastLocationPathname && lastLocationPathname !== pathname) {
|
||||||
|
Logger.markTimeOrigin();
|
||||||
|
}
|
||||||
|
lastLocationPathname = pathname;
|
||||||
|
}, [pathname]);
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RootComponent = () => (
|
||||||
|
<>
|
||||||
|
<ScrollToTop />
|
||||||
|
<LocationPathnameLogger />
|
||||||
|
<RootContextProviders>
|
||||||
|
<Menu
|
||||||
|
data={bootstrapData.common.menu_data}
|
||||||
|
isFrontendRoute={isFrontendRoute}
|
||||||
|
/>
|
||||||
|
<ExtensionsStartup>
|
||||||
|
<Outlet />
|
||||||
|
</ExtensionsStartup>
|
||||||
|
<ToastContainer />
|
||||||
|
</RootContextProviders>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const rootRoute = createRootRoute({ component: RootComponent });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a react-router v5 path pattern to TanStack syntax:
|
||||||
|
* ':param' segments become '$param' and trailing slashes are dropped
|
||||||
|
* (matching is trailing-slash insensitive via `trailingSlash: 'preserve'`).
|
||||||
|
*/
|
||||||
|
export const toTanstackPath = (path: string): string => {
|
||||||
|
const converted = path.replace(/:([A-Za-z0-9_]+)/g, '$$$1');
|
||||||
|
const trimmed = converted.replace(/\/+$/, '');
|
||||||
|
return trimmed === '' ? '/' : trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const seenPaths = new Set<string>();
|
||||||
|
const routeChildren = routes
|
||||||
|
.filter(({ path }) => {
|
||||||
|
const tanstackPath = toTanstackPath(path);
|
||||||
|
if (seenPaths.has(tanstackPath)) return false;
|
||||||
|
seenPaths.add(tanstackPath);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(({ path, Component, load, props = {}, Fallback = Loading }) =>
|
||||||
|
createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: toTanstackPath(path),
|
||||||
|
// Preloading a route (hover/focus intent) runs its loader, which
|
||||||
|
// warms the page's webpack chunk before the user commits to the
|
||||||
|
// navigation. The promise is deliberately NOT returned: loaders
|
||||||
|
// are awaited before render, and blocking would keep the shell
|
||||||
|
// (menu) from painting until the chunk lands. Fire-and-forget
|
||||||
|
// starts the fetch; the lazy() component suspends until it lands.
|
||||||
|
loader: load
|
||||||
|
? () => {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
component: () => (
|
||||||
|
<Suspense fallback={<Fallback />}>
|
||||||
|
<Layout>
|
||||||
|
<Layout.Content
|
||||||
|
css={css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<ErrorBoundary
|
||||||
|
css={css`
|
||||||
|
margin: 16px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Component user={bootstrapData.user} {...props} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout>
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const routeTree = rootRoute.addChildren(routeChildren);
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
routeTree,
|
||||||
|
basepath: applicationRoot() || undefined,
|
||||||
|
// Preload route chunks when the user shows intent (link hover/focus).
|
||||||
|
defaultPreload: 'intent',
|
||||||
|
defaultPreloadDelay: 50,
|
||||||
|
// Search params hold rison and pre-encoded payloads managed outside the
|
||||||
|
// router (use-query-params, direct history pushes); treat them as opaque
|
||||||
|
// strings that round-trip unchanged.
|
||||||
|
parseSearch,
|
||||||
|
stringifySearch,
|
||||||
|
trailingSlash: 'preserve',
|
||||||
|
// Unmatched paths rendered nothing under react-router's <Switch>; those
|
||||||
|
// URLs are owned by the Flask backend.
|
||||||
|
defaultNotFoundComponent: () => null,
|
||||||
|
scrollRestoration: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: the router type is deliberately NOT registered via
|
||||||
|
// `declare module '@tanstack/react-router' { interface Register ... }`.
|
||||||
|
// Registration would enforce strict route typing on every Link/navigate
|
||||||
|
// call, but Superset builds many URLs from backend-provided strings
|
||||||
|
// (e.g. dashboard.url); loose typing keeps those call sites valid.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user