mirror of
https://github.com/apache/superset.git
synced 2026-08-12 19:20:40 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
503933756e | ||
|
|
60261a5dc6 | ||
|
|
456512c508 | ||
|
|
c1d9b06649 | ||
|
|
7a64a82cd9 |
@@ -302,15 +302,6 @@ AUTH_USER_REGISTRATION = True
|
||||
AUTH_USER_REGISTRATION_ROLE = "Public"
|
||||
```
|
||||
|
||||
In case you want to assign the `Admin` role on new user registration, it can be assigned as follows:
|
||||
```python
|
||||
AUTH_USER_REGISTRATION_ROLE = "Admin"
|
||||
```
|
||||
If you encounter the [issue](https://github.com/apache/superset/issues/13243) of not being able to list users from the Superset main page settings, although a newly registered user has an `Admin` role, please re-run `superset init` to sync the required permissions. Below is the command to re-run `superset init` using docker compose.
|
||||
```
|
||||
docker-compose exec superset superset init
|
||||
```
|
||||
|
||||
Then, create a `CustomSsoSecurityManager` that extends `SupersetSecurityManager` and overrides
|
||||
`oauth_user_info`:
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ Here are the build presets that are exposed through the `supersetbot docker` uti
|
||||
- `py311`, e.g., Py311: Similar to lean but with a different Python version (in this example, 3.11).
|
||||
- `ci`: For certain CI workloads.
|
||||
- `websocket`: For Superset clusters supporting advanced features.
|
||||
- `dockerize`: Used by Helm in initContainers to wait for database dependencies to be available.
|
||||
- `dockerize`: Used by Helm.
|
||||
|
||||
## Key tags examples
|
||||
|
||||
|
||||
+7
-7
@@ -26,16 +26,16 @@
|
||||
"@emotion/styled": "^10.0.27",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@superset-ui/style": "^0.14.23",
|
||||
"antd": "^5.24.9",
|
||||
"antd": "^5.24.5",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"less": "^4.3.0",
|
||||
"less": "^4.2.2",
|
||||
"less-loader": "^11.0.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-svg-pan-zoom": "^3.13.1",
|
||||
"swagger-ui-react": "^5.21.0"
|
||||
"swagger-ui-react": "^5.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.7.0",
|
||||
@@ -44,12 +44,12 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react": "^7.0.0",
|
||||
"prettier": "^2.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"webpack": "^5.99.7"
|
||||
"typescript": "~5.8.2",
|
||||
"webpack": "^5.98.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
Vendored
+2064
-4219
File diff suppressed because it is too large
Load Diff
+146
-88
@@ -1092,13 +1092,20 @@
|
||||
core-js-pure "^3.30.2"
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.3", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.7", "@babel/runtime@^7.25.9", "@babel/runtime@^7.26.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.25.9", "@babel/runtime@^7.8.4":
|
||||
version "7.27.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762"
|
||||
integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2":
|
||||
version "7.26.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2"
|
||||
integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/template@^7.25.9", "@babel/template@^7.26.9", "@babel/template@^7.27.0":
|
||||
version "7.27.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4"
|
||||
@@ -3609,7 +3616,7 @@
|
||||
dependencies:
|
||||
"@types/istanbul-lib-report" "*"
|
||||
|
||||
"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
|
||||
"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
@@ -4055,11 +4062,16 @@ acorn-walk@^8.0.0:
|
||||
dependencies:
|
||||
acorn "^8.11.0"
|
||||
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2, acorn@^8.9.0:
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
|
||||
version "8.14.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
|
||||
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
|
||||
|
||||
acorn@^8.9.0:
|
||||
version "8.14.0"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz"
|
||||
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
|
||||
|
||||
address@^1.0.1, address@^1.1.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e"
|
||||
@@ -4179,10 +4191,10 @@ ansi-styles@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
antd@^5.24.9:
|
||||
version "5.24.9"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.24.9.tgz#c5862e02ed770bd95e312961f4f0b7b158a004d9"
|
||||
integrity sha512-liB+Y/JwD5/KSKbK1Z1EVAbWcoWYvWJ1s97AbbT+mOdigpJQuWwH7kG8IXNEljI7onvj0DdD43TXhSRLUu9AMA==
|
||||
antd@^5.24.5:
|
||||
version "5.24.5"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.24.5.tgz#b0976a113163888d1477f9e666c3c23352b098e9"
|
||||
integrity sha512-1lAv/G+9ewQanyoAo3JumQmIlVxwo5QwWGb6QCHYc40Cq0NxC/EzITcjsgq1PSaTUpLkKq8A2l7Fjtu47vqQBg==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^7.2.0"
|
||||
"@ant-design/cssinjs" "^1.23.0"
|
||||
@@ -4206,13 +4218,13 @@ antd@^5.24.9:
|
||||
rc-drawer "~7.2.0"
|
||||
rc-dropdown "~4.2.1"
|
||||
rc-field-form "~2.7.0"
|
||||
rc-image "~7.12.0"
|
||||
rc-input "~1.8.0"
|
||||
rc-input-number "~9.5.0"
|
||||
rc-mentions "~2.20.0"
|
||||
rc-image "~7.11.1"
|
||||
rc-input "~1.7.3"
|
||||
rc-input-number "~9.4.0"
|
||||
rc-mentions "~2.19.1"
|
||||
rc-menu "~9.16.1"
|
||||
rc-motion "^2.9.5"
|
||||
rc-notification "~5.6.4"
|
||||
rc-notification "~5.6.3"
|
||||
rc-pagination "~5.1.0"
|
||||
rc-picker "~4.11.3"
|
||||
rc-progress "~4.0.0"
|
||||
@@ -4224,8 +4236,8 @@ antd@^5.24.9:
|
||||
rc-steps "~6.0.1"
|
||||
rc-switch "~4.1.0"
|
||||
rc-table "~7.50.4"
|
||||
rc-tabs "~15.6.1"
|
||||
rc-textarea "~1.10.0"
|
||||
rc-tabs "~15.5.1"
|
||||
rc-textarea "~1.9.0"
|
||||
rc-tooltip "~6.4.0"
|
||||
rc-tree "~5.13.1"
|
||||
rc-tree-select "~5.27.0"
|
||||
@@ -4646,7 +4658,7 @@ call-bind@^1.0.7, call-bind@^1.0.8:
|
||||
get-intrinsic "^1.2.4"
|
||||
set-function-length "^1.2.2"
|
||||
|
||||
call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4:
|
||||
call-bound@^1.0.2, call-bound@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
|
||||
integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
|
||||
@@ -5667,7 +5679,12 @@ data-view-byte-offset@^1.0.1:
|
||||
es-errors "^1.3.0"
|
||||
is-data-view "^1.0.1"
|
||||
|
||||
dayjs@^1.11.11, dayjs@^1.11.13:
|
||||
dayjs@^1.11.11:
|
||||
version "1.11.12"
|
||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz"
|
||||
integrity sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==
|
||||
|
||||
dayjs@^1.11.13:
|
||||
version "1.11.13"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
|
||||
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
|
||||
@@ -6247,10 +6264,10 @@ escape-string-regexp@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
|
||||
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
|
||||
|
||||
eslint-config-prettier@^10.1.2:
|
||||
version "10.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz#31a4b393c40c4180202c27e829af43323bf85276"
|
||||
integrity sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==
|
||||
eslint-config-prettier@^10.1.1:
|
||||
version "10.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz#cf0ff6e5c4e7e15f129f1f1ce2a5ecba92dec132"
|
||||
integrity sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==
|
||||
|
||||
eslint-plugin-prettier@^4.0.0:
|
||||
version "4.2.1"
|
||||
@@ -6259,10 +6276,10 @@ eslint-plugin-prettier@^4.0.0:
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
|
||||
eslint-plugin-react@^7.37.5:
|
||||
version "7.37.5"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065"
|
||||
integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==
|
||||
eslint-plugin-react@^7.0.0:
|
||||
version "7.37.4"
|
||||
resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz"
|
||||
integrity sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==
|
||||
dependencies:
|
||||
array-includes "^3.1.8"
|
||||
array.prototype.findlast "^1.2.5"
|
||||
@@ -6274,7 +6291,7 @@ eslint-plugin-react@^7.37.5:
|
||||
hasown "^2.0.2"
|
||||
jsx-ast-utils "^2.4.1 || ^3.0.0"
|
||||
minimatch "^3.1.2"
|
||||
object.entries "^1.1.9"
|
||||
object.entries "^1.1.8"
|
||||
object.fromentries "^2.0.8"
|
||||
object.values "^1.2.1"
|
||||
prop-types "^15.8.1"
|
||||
@@ -8216,10 +8233,10 @@ less-loader@^11.0.0:
|
||||
resolved "https://registry.npmjs.org/less-loader/-/less-loader-11.1.4.tgz"
|
||||
integrity sha512-6/GrYaB6QcW6Vj+/9ZPgKKs6G10YZai/l/eJ4SLwbzqNTBsAqt5hSLVF47TgsiBxV1P6eAU0GYRH3YRuQU9V3A==
|
||||
|
||||
less@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.3.0.tgz#ef0cfc260a9ca8079ed8d0e3512bda8a12c82f2a"
|
||||
integrity sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==
|
||||
less@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.npmjs.org/less/-/less-4.2.2.tgz"
|
||||
integrity sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==
|
||||
dependencies:
|
||||
copy-anything "^2.0.1"
|
||||
parse-node-version "^1.0.1"
|
||||
@@ -9419,15 +9436,14 @@ object.assign@^4.1.0, object.assign@^4.1.4, object.assign@^4.1.7:
|
||||
has-symbols "^1.1.0"
|
||||
object-keys "^1.1.1"
|
||||
|
||||
object.entries@^1.1.9:
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3"
|
||||
integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==
|
||||
object.entries@^1.1.8:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz"
|
||||
integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==
|
||||
dependencies:
|
||||
call-bind "^1.0.8"
|
||||
call-bound "^1.0.4"
|
||||
call-bind "^1.0.7"
|
||||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.1.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
object.fromentries@^2.0.8:
|
||||
version "2.0.8"
|
||||
@@ -10676,10 +10692,10 @@ rc-field-form@~2.7.0:
|
||||
"@rc-component/async-validator" "^5.0.3"
|
||||
rc-util "^5.32.2"
|
||||
|
||||
rc-image@~7.12.0:
|
||||
version "7.12.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-7.12.0.tgz#95e9314701e668217d113c1f29b4f01ac025cafe"
|
||||
integrity sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==
|
||||
rc-image@~7.11.1:
|
||||
version "7.11.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-7.11.1.tgz#3ab290708dc053d3681de94186522e4e594f6772"
|
||||
integrity sha512-XuoWx4KUXg7hNy5mRTy1i8c8p3K8boWg6UajbHpDXS5AlRVucNfTi5YxTtPBTBzegxAZpvuLfh3emXFt6ybUdA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
"@rc-component/portal" "^1.0.2"
|
||||
@@ -10688,37 +10704,37 @@ rc-image@~7.12.0:
|
||||
rc-motion "^2.6.2"
|
||||
rc-util "^5.34.1"
|
||||
|
||||
rc-input-number@~9.5.0:
|
||||
version "9.5.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-9.5.0.tgz#b47963d0f2cbd85ab2f1badfdc089a904c073f38"
|
||||
integrity sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==
|
||||
rc-input-number@~9.4.0:
|
||||
version "9.4.0"
|
||||
resolved "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.4.0.tgz"
|
||||
integrity sha512-Tiy4DcXcFXAf9wDhN8aUAyMeCLHJUHA/VA/t7Hj8ZEx5ETvxG7MArDOSE6psbiSCo+vJPm4E3fGN710ITVn6GA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
"@rc-component/mini-decimal" "^1.0.1"
|
||||
classnames "^2.2.5"
|
||||
rc-input "~1.8.0"
|
||||
rc-input "~1.7.1"
|
||||
rc-util "^5.40.1"
|
||||
|
||||
rc-input@~1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-1.8.0.tgz#d2f4404befebf2fbdc28390d5494c302f74ae974"
|
||||
integrity sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==
|
||||
rc-input@~1.7.1, rc-input@~1.7.3:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-1.7.3.tgz#cb334a17b93ce985bceb243b4c111a5ed641e0e3"
|
||||
integrity sha512-A5w4egJq8+4JzlQ55FfQjDnPvOaAbzwC3VLOAdOytyek3TboSOP9qxN+Gifup+shVXfvecBLBbWBpWxmk02SWQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.1"
|
||||
classnames "^2.2.1"
|
||||
rc-util "^5.18.1"
|
||||
|
||||
rc-mentions@~2.20.0:
|
||||
version "2.20.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-mentions/-/rc-mentions-2.20.0.tgz#3bbeac0352b02e0ce3e1244adb48701bb6903bf7"
|
||||
integrity sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==
|
||||
rc-mentions@~2.19.1:
|
||||
version "2.19.1"
|
||||
resolved "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.19.1.tgz"
|
||||
integrity sha512-KK3bAc/bPFI993J3necmaMXD2reZTzytZdlTvkeBbp50IGH1BDPDvxLdHDUrpQx2b2TGaVJsn+86BvYa03kGqA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.22.5"
|
||||
"@rc-component/trigger" "^2.0.0"
|
||||
classnames "^2.2.6"
|
||||
rc-input "~1.8.0"
|
||||
rc-input "~1.7.1"
|
||||
rc-menu "~9.16.0"
|
||||
rc-textarea "~1.10.0"
|
||||
rc-textarea "~1.9.0"
|
||||
rc-util "^5.34.1"
|
||||
|
||||
rc-menu@~9.16.0, rc-menu@~9.16.1:
|
||||
@@ -10742,10 +10758,10 @@ rc-motion@^2.0.0, rc-motion@^2.0.1, rc-motion@^2.3.0, rc-motion@^2.3.4, rc-motio
|
||||
classnames "^2.2.1"
|
||||
rc-util "^5.44.0"
|
||||
|
||||
rc-notification@~5.6.4:
|
||||
version "5.6.4"
|
||||
resolved "https://registry.yarnpkg.com/rc-notification/-/rc-notification-5.6.4.tgz#ea89c39c13cd517fdfd97fe63f03376fabb78544"
|
||||
integrity sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==
|
||||
rc-notification@~5.6.3:
|
||||
version "5.6.3"
|
||||
resolved "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.3.tgz"
|
||||
integrity sha512-42szwnn8VYQoT6GnjO00i1iwqV9D1TTMvxObWsuLwgl0TsOokzhkYiufdtQBsJMFjJravS1hfDKVMHLKLcPE4g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "2.x"
|
||||
@@ -10873,10 +10889,10 @@ rc-table@~7.50.4:
|
||||
rc-util "^5.44.3"
|
||||
rc-virtual-list "^3.14.2"
|
||||
|
||||
rc-tabs@~15.6.1:
|
||||
version "15.6.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-15.6.1.tgz#f0b6c65384dfa09a64eb539e86a0667c7a650708"
|
||||
integrity sha512-/HzDV1VqOsUWyuC0c6AkxVYFjvx9+rFPKZ32ejxX0Uc7QCzcEjTA9/xMgv4HemPKwzBNX8KhGVbbumDjnj92aA==
|
||||
rc-tabs@~15.5.1:
|
||||
version "15.5.1"
|
||||
resolved "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.1.tgz"
|
||||
integrity sha512-yiWivLAjEo5d1v2xlseB2dQocsOhkoVSfo1krS8v8r+02K+TBUjSjXIf7dgyVSxp6wRIPv5pMi5hanNUlQMgUA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
classnames "2.x"
|
||||
@@ -10886,14 +10902,14 @@ rc-tabs@~15.6.1:
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.34.1"
|
||||
|
||||
rc-textarea@~1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.0.tgz#f8f962ef83be0b8e35db97cf03dbfb86ddd9c46c"
|
||||
integrity sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==
|
||||
rc-textarea@~1.9.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.9.0.tgz"
|
||||
integrity sha512-dQW/Bc/MriPBTugj2Kx9PMS5eXCCGn2cxoIaichjbNvOiARlaHdI99j4DTxLl/V8+PIfW06uFy7kjfUIDDKyxQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.1"
|
||||
rc-input "~1.8.0"
|
||||
rc-input "~1.7.1"
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.27.0"
|
||||
|
||||
@@ -11527,7 +11543,7 @@ resolve-pathname@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
|
||||
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
||||
|
||||
resolve@^1.1.6, resolve@^1.12.0, resolve@^1.14.2:
|
||||
resolve@^1.1.6, resolve@^1.14.2:
|
||||
version "1.22.10"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
|
||||
integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
|
||||
@@ -11536,6 +11552,15 @@ resolve@^1.1.6, resolve@^1.12.0, resolve@^1.14.2:
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
resolve@^1.12.0:
|
||||
version "1.22.8"
|
||||
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz"
|
||||
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
|
||||
dependencies:
|
||||
is-core-module "^2.13.0"
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
resolve@^2.0.0-next.5:
|
||||
version "2.0.0-next.5"
|
||||
resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz"
|
||||
@@ -11684,10 +11709,10 @@ schema-utils@^3.0.0:
|
||||
ajv "^6.12.5"
|
||||
ajv-keywords "^3.5.2"
|
||||
|
||||
schema-utils@^4.0.0, schema-utils@^4.0.1, schema-utils@^4.3.0, schema-utils@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae"
|
||||
integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==
|
||||
schema-utils@^4.0.0, schema-utils@^4.0.1, schema-utils@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0"
|
||||
integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.9"
|
||||
ajv "^8.9.0"
|
||||
@@ -12334,10 +12359,10 @@ swagger-client@^3.34.4:
|
||||
ramda "^0.30.1"
|
||||
ramda-adjunct "^5.1.0"
|
||||
|
||||
swagger-ui-react@^5.21.0:
|
||||
version "5.21.0"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.21.0.tgz#a9b22a24247c23eafb39b776bee5a4d5b56c57fe"
|
||||
integrity sha512-lS5paITM1kkcBb/BTTSMHKelh8elHfcuUP4T3R3mO80tDR0AYJL2HR5UdQD6nV1LwdvekzRM8gKjJA6hVayi0A==
|
||||
swagger-ui-react@^5.20.2:
|
||||
version "5.20.2"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.20.2.tgz#eb9f8de2e6c916661d8033d43119f6e105ab87da"
|
||||
integrity sha512-6ifaFjT02yBv1kjEivIMWxQpI7r8O7D/oA8u1JiwhTkom0dOk85lTExao5Dj5ztS6dBg6i1zm+ILhH94fF9g8Q==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@scarf/scarf" "=1.4.0"
|
||||
@@ -12515,11 +12540,16 @@ tslib@^1.8.1:
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.3, tslib@^2.3.0, tslib@^2.6.0:
|
||||
tslib@^2.0.3, tslib@^2.6.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tslib@^2.3.0:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
|
||||
@@ -12621,10 +12651,10 @@ types-ramda@^0.30.0:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript@~5.8.3:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
|
||||
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
||||
typescript@~5.8.2:
|
||||
version "5.8.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
|
||||
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
|
||||
|
||||
ufo@^1.5.4:
|
||||
version "1.6.1"
|
||||
@@ -13033,14 +13063,13 @@ webpack-sources@^3.2.3:
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||
|
||||
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.7:
|
||||
version "5.99.7"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.7.tgz#60201c1ca66da046b07d006c2f6e0cc5e8a7bdba"
|
||||
integrity sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==
|
||||
webpack@^5.88.1:
|
||||
version "5.99.5"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.5.tgz#86e3b3a5a03377ea5da271c929934003f5ac5dd8"
|
||||
integrity sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.6"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
"@webassemblyjs/ast" "^1.14.1"
|
||||
"@webassemblyjs/wasm-edit" "^1.14.1"
|
||||
"@webassemblyjs/wasm-parser" "^1.14.1"
|
||||
@@ -13057,7 +13086,36 @@ webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.7:
|
||||
loader-runner "^4.2.0"
|
||||
mime-types "^2.1.27"
|
||||
neo-async "^2.6.2"
|
||||
schema-utils "^4.3.2"
|
||||
schema-utils "^4.3.0"
|
||||
tapable "^2.1.1"
|
||||
terser-webpack-plugin "^5.3.11"
|
||||
watchpack "^2.4.1"
|
||||
webpack-sources "^3.2.3"
|
||||
|
||||
webpack@^5.95.0, webpack@^5.98.0:
|
||||
version "5.98.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.98.0.tgz#44ae19a8f2ba97537978246072fb89d10d1fbd17"
|
||||
integrity sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.6"
|
||||
"@webassemblyjs/ast" "^1.14.1"
|
||||
"@webassemblyjs/wasm-edit" "^1.14.1"
|
||||
"@webassemblyjs/wasm-parser" "^1.14.1"
|
||||
acorn "^8.14.0"
|
||||
browserslist "^4.24.0"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.17.1"
|
||||
es-module-lexer "^1.2.1"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
glob-to-regexp "^0.4.1"
|
||||
graceful-fs "^4.2.11"
|
||||
json-parse-even-better-errors "^2.3.1"
|
||||
loader-runner "^4.2.0"
|
||||
mime-types "^2.1.27"
|
||||
neo-async "^2.6.2"
|
||||
schema-utils "^4.3.0"
|
||||
tapable "^2.1.1"
|
||||
terser-webpack-plugin "^5.3.11"
|
||||
watchpack "^2.4.1"
|
||||
|
||||
+2
-4
@@ -44,7 +44,7 @@ dependencies = [
|
||||
"cryptography>=42.0.4, <45.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <3.0.0",
|
||||
"flask-appbuilder>=4.6.3, <5.0.0",
|
||||
"flask-appbuilder>=4.6.1, <5.0.0",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
@@ -81,7 +81,7 @@ dependencies = [
|
||||
"python-dateutil",
|
||||
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
|
||||
"python-geohash",
|
||||
"pyarrow>=18.1.0, <19",
|
||||
"pyarrow>=14.0.1, <15",
|
||||
"pyyaml>=6.0.0, <7.0.0",
|
||||
"PyJWT>=2.4.0, <3.0",
|
||||
"redis>=4.6.0, <5.0",
|
||||
@@ -371,14 +371,12 @@ authorized_licenses = [
|
||||
"apache software",
|
||||
"apache software, bsd",
|
||||
"bsd",
|
||||
"bsd-3-clause",
|
||||
"isc license (iscl)",
|
||||
"isc license",
|
||||
"mit",
|
||||
"mozilla public license 2.0 (mpl 2.0)",
|
||||
"osi approved",
|
||||
"osi approved",
|
||||
"psf-2.0",
|
||||
"python software foundation",
|
||||
"the unlicense (unlicense)",
|
||||
"the unlicense",
|
||||
|
||||
@@ -82,7 +82,7 @@ cron-descriptor==1.4.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
croniter==6.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
cryptography==44.0.3
|
||||
cryptography==44.0.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# paramiko
|
||||
@@ -118,7 +118,7 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.6.3
|
||||
flask-appbuilder==4.6.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-babel==2.0.0
|
||||
# via flask-appbuilder
|
||||
@@ -161,13 +161,13 @@ greenlet==3.1.1
|
||||
# sqlalchemy
|
||||
gunicorn==23.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
h11==0.16.0
|
||||
h11==0.14.0
|
||||
# via wsproto
|
||||
hashids==1.3.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
holidays==0.25
|
||||
# via apache-superset (pyproject.toml)
|
||||
humanize==4.12.3
|
||||
humanize==4.12.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
idna==3.10
|
||||
# via
|
||||
@@ -202,7 +202,7 @@ mako==1.3.10
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# alembic
|
||||
markdown==3.8
|
||||
markdown==3.7
|
||||
# via apache-superset (pyproject.toml)
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
@@ -236,6 +236,7 @@ numpy==1.26.4
|
||||
# bottleneck
|
||||
# numexpr
|
||||
# pandas
|
||||
# pyarrow
|
||||
odfpy==1.4.1
|
||||
# via pandas
|
||||
openpyxl==3.1.5
|
||||
@@ -273,7 +274,7 @@ prison==0.2.1
|
||||
# via flask-appbuilder
|
||||
prompt-toolkit==3.0.51
|
||||
# via click-repl
|
||||
pyarrow==18.1.0
|
||||
pyarrow==14.0.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
pyasn1==0.6.1
|
||||
# via
|
||||
@@ -373,7 +374,7 @@ sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
sqlglot==26.16.4
|
||||
sqlglot==26.16.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
sqlparse==0.5.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
|
||||
@@ -138,7 +138,7 @@ croniter==6.0.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
cryptography==44.0.3
|
||||
cryptography==44.0.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -202,7 +202,7 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.6.3
|
||||
flask-appbuilder==4.6.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -330,7 +330,7 @@ gunicorn==23.0.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
h11==0.16.0
|
||||
h11==0.14.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# wsproto
|
||||
@@ -343,7 +343,7 @@ holidays==0.25
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# prophet
|
||||
humanize==4.12.3
|
||||
humanize==4.12.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -415,7 +415,7 @@ mako==1.3.10
|
||||
# -c requirements/base.txt
|
||||
# alembic
|
||||
# apache-superset
|
||||
markdown==3.8
|
||||
markdown==3.7
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -473,6 +473,7 @@ numpy==1.26.4
|
||||
# pandas
|
||||
# pandas-gbq
|
||||
# prophet
|
||||
# pyarrow
|
||||
oauthlib==3.2.2
|
||||
# via requests-oauthlib
|
||||
odfpy==1.4.1
|
||||
@@ -586,7 +587,7 @@ psutil==6.1.0
|
||||
# via apache-superset
|
||||
psycopg2-binary==2.9.6
|
||||
# via apache-superset
|
||||
pyarrow==18.1.0
|
||||
pyarrow==14.0.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -799,7 +800,7 @@ sqlalchemy-utils==0.38.3
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
sqlglot==26.16.4
|
||||
sqlglot==26.16.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -252,215 +252,4 @@ describe('Visualization > Table', () => {
|
||||
});
|
||||
cy.get('td').contains(/\d*%/);
|
||||
});
|
||||
|
||||
it('Test row limit with server pagination toggle', () => {
|
||||
cy.visitChartByParams({
|
||||
...VIZ_DEFAULTS,
|
||||
metrics: ['count'],
|
||||
row_limit: 100,
|
||||
});
|
||||
|
||||
// Enable server pagination
|
||||
cy.get('[data-test="server_pagination-header"] div.pull-left').click();
|
||||
|
||||
// Click row limit control and select high value (200k)
|
||||
cy.get('div[aria-label="Row limit"]').click();
|
||||
|
||||
// Type 200000 and press enter to select the option
|
||||
cy.get('div[aria-label="Row limit"]')
|
||||
.find('.ant-select-selection-search-input:visible')
|
||||
.type('200000{enter}');
|
||||
|
||||
// Verify that there is no error tooltip when server pagination is enabled
|
||||
cy.get('[data-test="error-tooltip"]').should('not.exist');
|
||||
|
||||
// Disable server pagination
|
||||
cy.get('[data-test="server_pagination-header"] div.pull-left').click();
|
||||
|
||||
// Verify error tooltip appears
|
||||
cy.get('[data-test="error-tooltip"]').should('be.visible');
|
||||
|
||||
// Trigger mouseover and verify tooltip text
|
||||
cy.get('[data-test="error-tooltip"]').trigger('mouseover');
|
||||
|
||||
// Verify tooltip content
|
||||
cy.get('.antd5-tooltip-inner').should('be.visible');
|
||||
cy.get('.antd5-tooltip-inner').should(
|
||||
'contain',
|
||||
'Server pagination needs to be enabled for values over',
|
||||
);
|
||||
|
||||
// Hide the tooltip by adding display:none style
|
||||
cy.get('.antd5-tooltip').invoke('attr', 'style', 'display: none');
|
||||
|
||||
// Enable server pagination again
|
||||
cy.get('[data-test="server_pagination-header"] div.pull-left').click();
|
||||
|
||||
cy.get('[data-test="error-tooltip"]').should('not.exist');
|
||||
|
||||
cy.get('div[aria-label="Row limit"]').click();
|
||||
|
||||
// Type 1000000
|
||||
cy.get('div[aria-label="Row limit"]')
|
||||
.find('.ant-select-selection-search-input:visible')
|
||||
.type('1000000');
|
||||
|
||||
// Wait for 1 second
|
||||
cy.wait(1000);
|
||||
|
||||
// Press enter
|
||||
cy.get('div[aria-label="Row limit"]')
|
||||
.find('.ant-select-selection-search-input:visible')
|
||||
.type('{enter}');
|
||||
|
||||
// Wait for error tooltip to appear and verify its content
|
||||
cy.get('[data-test="error-tooltip"]')
|
||||
.should('be.visible')
|
||||
.trigger('mouseover');
|
||||
|
||||
// Wait for tooltip content and verify
|
||||
cy.get('.antd5-tooltip-inner').should('exist');
|
||||
cy.get('.antd5-tooltip-inner').should('be.visible');
|
||||
|
||||
// Verify tooltip content separately
|
||||
cy.get('.antd5-tooltip-inner').should('contain', 'Value cannot exceed');
|
||||
});
|
||||
|
||||
it('Test sorting with server pagination enabled', () => {
|
||||
cy.visitChartByParams({
|
||||
...VIZ_DEFAULTS,
|
||||
metrics: ['count'],
|
||||
groupby: ['name'],
|
||||
row_limit: 100000,
|
||||
server_pagination: true, // Enable server pagination
|
||||
});
|
||||
|
||||
// Wait for the initial data load
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Get the first column header (name)
|
||||
cy.get('.chart-container th').contains('name').as('nameHeader');
|
||||
|
||||
// Click to sort ascending
|
||||
cy.get('@nameHeader').click();
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Verify first row starts with 'A'
|
||||
cy.get('.chart-container td:first').invoke('text').should('match', /^[Aa]/);
|
||||
|
||||
// Click again to sort descending
|
||||
cy.get('@nameHeader').click();
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Verify first row starts with 'Z'
|
||||
cy.get('.chart-container td:first').invoke('text').should('match', /^[Zz]/);
|
||||
|
||||
// Test numeric sorting
|
||||
cy.get('.chart-container th').contains('COUNT').as('countHeader');
|
||||
|
||||
// Click to sort ascending by count
|
||||
cy.get('@countHeader').click();
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Get first two count values and verify ascending order
|
||||
cy.get('.chart-container td:nth-child(2)').then($cells => {
|
||||
const first = parseFloat($cells[0].textContent || '0');
|
||||
const second = parseFloat($cells[1].textContent || '0');
|
||||
expect(first).to.be.at.most(second);
|
||||
});
|
||||
|
||||
// Click again to sort descending
|
||||
cy.get('@countHeader').click();
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Get first two count values and verify descending order
|
||||
cy.get('.chart-container td:nth-child(2)').then($cells => {
|
||||
const first = parseFloat($cells[0].textContent || '0');
|
||||
const second = parseFloat($cells[1].textContent || '0');
|
||||
expect(first).to.be.at.least(second);
|
||||
});
|
||||
});
|
||||
|
||||
it('Test search with server pagination enabled', () => {
|
||||
cy.visitChartByParams({
|
||||
...VIZ_DEFAULTS,
|
||||
metrics: ['count'],
|
||||
groupby: ['name', 'state'],
|
||||
row_limit: 100000,
|
||||
server_pagination: true,
|
||||
include_search: true,
|
||||
});
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Basic search test
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').should(
|
||||
'be.visible',
|
||||
);
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').type('John');
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
cy.get('.chart-container tbody tr').each($row => {
|
||||
cy.wrap($row).contains(/John/i);
|
||||
});
|
||||
|
||||
// Clear and test case-insensitive search
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').clear();
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').type('mary');
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
cy.get('.chart-container tbody tr').each($row => {
|
||||
cy.wrap($row).contains(/Mary/i);
|
||||
});
|
||||
|
||||
// Test special characters
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').clear();
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').type('Nicole');
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
cy.get('.chart-container tbody tr').each($row => {
|
||||
cy.wrap($row).contains(/Nicole/i);
|
||||
});
|
||||
|
||||
// Test no results
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').clear();
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').type('XYZ123');
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
cy.get('.chart-container').contains('No records found');
|
||||
|
||||
// Test column-specific search
|
||||
cy.get('.search-select').should('be.visible');
|
||||
|
||||
cy.get('.search-select').click();
|
||||
|
||||
cy.get('.ant-select-dropdown').should('be.visible');
|
||||
|
||||
cy.get('.ant-select-item-option').contains('state').should('be.visible');
|
||||
|
||||
cy.get('.ant-select-item-option').contains('state').click();
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').clear();
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').type('CA');
|
||||
|
||||
cy.wait('@chartData');
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get('td[aria-labelledby="header-state"]').should('be.visible');
|
||||
|
||||
cy.get('td[aria-labelledby="header-state"]')
|
||||
.first()
|
||||
.should('contain', 'CA');
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+62
-76
@@ -23,7 +23,7 @@
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/core": "^5.21.1",
|
||||
"@rjsf/utils": "^5.24.3",
|
||||
"@rjsf/validator-ajv8": "^5.24.9",
|
||||
"@rjsf/validator-ajv8": "^5.22.3",
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
|
||||
"@superset-ui/core": "file:./packages/superset-ui-core",
|
||||
@@ -115,7 +115,7 @@
|
||||
"react-dom": "^17.0.2",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-intersection-observer": "^9.10.2",
|
||||
"react-js-cron": "^2.1.2",
|
||||
"react-json-tree": "^0.17.0",
|
||||
"react-lines-ellipsis": "^0.15.4",
|
||||
@@ -162,7 +162,7 @@
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
||||
"@babel/plugin-transform-runtime": "^7.27.1",
|
||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
||||
"@babel/preset-env": "^7.26.7",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
@@ -1146,14 +1146,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.25.9",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1240,13 +1240,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
|
||||
"integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
|
||||
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/types": "^7.27.1",
|
||||
"@babel/parser": "^7.26.5",
|
||||
"@babel/types": "^7.26.5",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -1387,13 +1387,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
|
||||
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/traverse": "^7.25.9",
|
||||
"@babel/types": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1431,9 +1431,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
||||
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
|
||||
"integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1491,18 +1491,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1572,12 +1572,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz",
|
||||
"integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==",
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
|
||||
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/types": "^7.26.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -2849,16 +2849,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-runtime": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.27.1.tgz",
|
||||
"integrity": "sha512-TqGF3desVsTcp3WrJGj4HfKokfCXCLcHpt4PJF0D8/iT6LPd9RS82Upw3KPeyr6B22Lfd3DO8MVrmp0oRkUDdw==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz",
|
||||
"integrity": "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-module-imports": "^7.25.9",
|
||||
"@babel/helper-plugin-utils": "^7.25.9",
|
||||
"babel-plugin-polyfill-corejs2": "^0.4.10",
|
||||
"babel-plugin-polyfill-corejs3": "^0.11.0",
|
||||
"babel-plugin-polyfill-corejs3": "^0.10.6",
|
||||
"babel-plugin-polyfill-regenerator": "^0.6.1",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
@@ -2869,20 +2869,6 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs3": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
|
||||
"integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.3",
|
||||
"core-js-compat": "^3.40.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-runtime/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
@@ -3293,30 +3279,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz",
|
||||
"integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
||||
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/code-frame": "^7.25.9",
|
||||
"@babel/parser": "^7.25.9",
|
||||
"@babel/types": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz",
|
||||
"integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
|
||||
"integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.1",
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/template": "^7.27.1",
|
||||
"@babel/types": "^7.27.1",
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/generator": "^7.26.5",
|
||||
"@babel/parser": "^7.26.7",
|
||||
"@babel/template": "^7.25.9",
|
||||
"@babel/types": "^7.26.7",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
@@ -3325,13 +3311,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
|
||||
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
|
||||
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -8512,9 +8498,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rjsf/validator-ajv8": {
|
||||
"version": "5.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.9.tgz",
|
||||
"integrity": "sha512-leHb39Qa612QhAfvw36qi/ubWa7LQ6hrPN4Ge93QBlWywRfV/M0Wmx9bPccCGgIL4Qnn1Wmt53EWV8kQT28xTA==",
|
||||
"version": "5.24.1",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.1.tgz",
|
||||
"integrity": "sha512-p6URehglU9yFUAoQXE1ryqZjLYSjc6qdbiUfCVvEFAzUuMECsIFomz2hH3CPlt10K72sAFdzwVvrKn1iWTnxDw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ajv": "^8.12.0",
|
||||
@@ -39490,9 +39476,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-intersection-observer": {
|
||||
"version": "9.16.0",
|
||||
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz",
|
||||
"integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==",
|
||||
"version": "9.15.1",
|
||||
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.15.1.tgz",
|
||||
"integrity": "sha512-vGrqYEVWXfH+AGu241uzfUpNK4HAdhCkSAyFdkMb9VWWXs6mxzBLpWCxEy9YcnDNY2g9eO6z7qUtTBdA9hc8pA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/core": "^5.21.1",
|
||||
"@rjsf/utils": "^5.24.3",
|
||||
"@rjsf/validator-ajv8": "^5.24.9",
|
||||
"@rjsf/validator-ajv8": "^5.22.3",
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
|
||||
"@superset-ui/core": "file:./packages/superset-ui-core",
|
||||
@@ -182,7 +182,7 @@
|
||||
"react-dom": "^17.0.2",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-intersection-observer": "^9.10.2",
|
||||
"react-js-cron": "^2.1.2",
|
||||
"react-json-tree": "^0.17.0",
|
||||
"react-lines-ellipsis": "^0.15.4",
|
||||
@@ -229,7 +229,7 @@
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
||||
"@babel/plugin-transform-runtime": "^7.27.1",
|
||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
||||
"@babel/preset-env": "^7.26.7",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
import { ColumnMeta, SortSeriesData, SortSeriesType } from './types';
|
||||
|
||||
export const DEFAULT_MAX_ROW = 100000;
|
||||
export const DEFAULT_MAX_ROW_TABLE_SERVER = 500000;
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const TIME_FILTER_LABELS = {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Dataset } from './types';
|
||||
|
||||
export const TestDataset: Dataset = {
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
columns: [
|
||||
{
|
||||
advanced_data_type: undefined,
|
||||
|
||||
@@ -69,6 +69,7 @@ export interface Dataset {
|
||||
columns: ColumnMeta[];
|
||||
metrics: Metric[];
|
||||
column_formats: Record<string, string>;
|
||||
currency_formats: Record<string, Currency>;
|
||||
verbose_map: Record<string, string>;
|
||||
main_dttm_col: string;
|
||||
// eg. ['["ds", true]', 'ds [asc]']
|
||||
|
||||
+2
@@ -53,6 +53,7 @@ describe('columnChoices()', () => {
|
||||
],
|
||||
verbose_map: {},
|
||||
column_formats: { fiz: 'NUMERIC', about: 'STRING', foo: 'DATE' },
|
||||
currency_formats: {},
|
||||
datasource_name: 'my_datasource',
|
||||
description: 'this is my datasource',
|
||||
}),
|
||||
@@ -104,6 +105,7 @@ describe('columnChoices()', () => {
|
||||
],
|
||||
verbose_map: {},
|
||||
column_formats: { fiz: 'NUMERIC', about: 'STRING', foo: 'DATE' },
|
||||
currency_formats: {},
|
||||
datasource_name: 'my_datasource',
|
||||
description: 'this is my datasource',
|
||||
}),
|
||||
|
||||
+1
@@ -41,6 +41,7 @@ describe('defineSavedMetrics', () => {
|
||||
columns: [],
|
||||
verbose_map: {},
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
datasource_name: 'my_datasource',
|
||||
description: 'this is my datasource',
|
||||
};
|
||||
|
||||
@@ -25,4 +25,3 @@ export { default as validateNonEmpty } from './validateNonEmpty';
|
||||
export { default as validateMaxValue } from './validateMaxValue';
|
||||
export { default as validateMapboxStylesUrl } from './validateMapboxStylesUrl';
|
||||
export { default as validateTimeComparisonRangeValues } from './validateTimeComparisonRangeValues';
|
||||
export { default as validateServerPagination } from './validateServerPagination';
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '../translation';
|
||||
|
||||
export default function validateServerPagination(
|
||||
v: unknown,
|
||||
serverPagination: boolean,
|
||||
max: number,
|
||||
) {
|
||||
if (Number(v) > +max && !serverPagination) {
|
||||
return t('Server pagination needs to be enabled for values over %s', max);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { validateServerPagination } from '@superset-ui/core';
|
||||
import './setup';
|
||||
|
||||
test('validateServerPagination returns warning message when server pagination is disabled and value exceeds max', () => {
|
||||
expect(validateServerPagination(100001, false, 100000)).toBeTruthy();
|
||||
expect(validateServerPagination('150000', false, 100000)).toBeTruthy();
|
||||
expect(validateServerPagination(200000, false, 100000)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('validateServerPagination returns false when server pagination is enabled', () => {
|
||||
expect(validateServerPagination(100001, true, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(150000, true, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination('200000', true, 100000)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('validateServerPagination returns false when value is below max', () => {
|
||||
expect(validateServerPagination(50000, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination('75000', false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(99999, false, 100000)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('validateServerPagination handles edge cases', () => {
|
||||
expect(validateServerPagination(undefined, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(null, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(NaN, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination('invalid', false, 100000)).toBeFalsy();
|
||||
});
|
||||
@@ -60,7 +60,7 @@
|
||||
"@storybook/react-webpack5": "8.2.9",
|
||||
"babel-loader": "^9.1.3",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
+1
-22
@@ -36,25 +36,13 @@ import {
|
||||
} from './types';
|
||||
import { useOverflowDetection } from './useOverflowDetection';
|
||||
|
||||
const MetricNameText = styled.div<{ metricNameFontSize?: number }>`
|
||||
${({ theme, metricNameFontSize }) => `
|
||||
font-family: ${theme.typography.families.sansSerif};
|
||||
font-weight: ${theme.typography.weights.normal};
|
||||
font-size: ${metricNameFontSize || theme.typography.sizes.s * 2}px;
|
||||
text-align: center;
|
||||
margin-bottom: ${theme.gridUnit * 3}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
const NumbersContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
`;
|
||||
|
||||
const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
|
||||
@@ -85,8 +73,6 @@ export default function PopKPI(props: PopKPIProps) {
|
||||
prevNumber,
|
||||
valueDifference,
|
||||
percentDifferenceFormattedString,
|
||||
metricName,
|
||||
metricNameFontSize,
|
||||
headerFontSize,
|
||||
subheaderFontSize,
|
||||
comparisonColorEnabled,
|
||||
@@ -98,8 +84,8 @@ export default function PopKPI(props: PopKPIProps) {
|
||||
subtitle,
|
||||
subtitleFontSize,
|
||||
dashboardTimeRange,
|
||||
showMetricName,
|
||||
} = props;
|
||||
|
||||
const [comparisonRange, setComparisonRange] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -274,16 +260,9 @@ export default function PopKPI(props: PopKPIProps) {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
align-items: flex-start;
|
||||
overflow: auto;
|
||||
`
|
||||
}
|
||||
>
|
||||
{showMetricName && metricName && (
|
||||
<MetricNameText metricNameFontSize={metricNameFontSize}>
|
||||
{metricName}
|
||||
</MetricNameText>
|
||||
)}
|
||||
|
||||
<div css={bigValueContainerStyles}>
|
||||
{bigNumber}
|
||||
{percentDifferenceNumber !== 0 && (
|
||||
|
||||
-4
@@ -28,8 +28,6 @@ import {
|
||||
subheaderFontSize,
|
||||
subtitleControl,
|
||||
subtitleFontSize,
|
||||
showMetricNameControl,
|
||||
metricNameFontSizeWithVisibility,
|
||||
} from '../sharedControls';
|
||||
import { ColorSchemeEnum } from './types';
|
||||
|
||||
@@ -72,8 +70,6 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
[subtitleControl],
|
||||
[subtitleFontSize],
|
||||
[showMetricNameControl],
|
||||
[metricNameFontSizeWithVisibility],
|
||||
[
|
||||
{
|
||||
...subheaderFontSize,
|
||||
|
||||
-1
@@ -32,7 +32,6 @@ export default class PopKPIPlugin extends ChartPlugin {
|
||||
tags: [
|
||||
t('Comparison'),
|
||||
t('Business'),
|
||||
t('ECharts'),
|
||||
t('Percentages'),
|
||||
t('Report'),
|
||||
t('Advanced-Analytics'),
|
||||
|
||||
+3
-15
@@ -26,13 +26,7 @@ import {
|
||||
SimpleAdhocFilter,
|
||||
ensureIsArray,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
getComparisonFontSize,
|
||||
getHeaderFontSize,
|
||||
getMetricNameFontSize,
|
||||
} from './utils';
|
||||
|
||||
import { getOriginalLabel } from '../utils';
|
||||
import { getComparisonFontSize, getHeaderFontSize } from './utils';
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
@@ -89,7 +83,6 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
headerFontSize,
|
||||
headerText,
|
||||
metric,
|
||||
metricNameFontSize,
|
||||
yAxisFormat,
|
||||
currencyFormat,
|
||||
subheaderFontSize,
|
||||
@@ -98,14 +91,11 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
percentDifferenceFormat,
|
||||
subtitle = '',
|
||||
subtitleFontSize,
|
||||
columnConfig = {},
|
||||
columnConfig,
|
||||
} = formData;
|
||||
const { data: dataA = [] } = queriesData[0];
|
||||
const data = dataA;
|
||||
const metricName = metric ? getMetricLabel(metric) : '';
|
||||
const metrics = chartProps.datasource?.metrics || [];
|
||||
const originalLabel = getOriginalLabel(metric, metrics);
|
||||
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
||||
const timeComparison = ensureIsArray(chartProps.rawFormData?.time_compare)[0];
|
||||
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
||||
const currentTimeRangeFilter = chartProps.rawFormData?.adhoc_filters?.filter(
|
||||
@@ -189,7 +179,7 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
metricName: originalLabel,
|
||||
metricName,
|
||||
bigNumber,
|
||||
prevNumber,
|
||||
valueDifference,
|
||||
@@ -197,8 +187,6 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
boldText,
|
||||
subtitle,
|
||||
subtitleFontSize,
|
||||
showMetricName,
|
||||
metricNameFontSize: getMetricNameFontSize(metricNameFontSize),
|
||||
headerFontSize: getHeaderFontSize(headerFontSize),
|
||||
subheaderFontSize: getComparisonFontSize(subheaderFontSize),
|
||||
headerText,
|
||||
|
||||
-2
@@ -61,8 +61,6 @@ export type PopKPIProps = PopKPIStylesProps &
|
||||
data: TimeseriesDataRecord[];
|
||||
metrics: Metric[];
|
||||
metricName: string;
|
||||
metricNameFontSize?: number;
|
||||
showMetricName: boolean;
|
||||
bigNumber: string;
|
||||
prevNumber: string;
|
||||
subtitle?: string;
|
||||
|
||||
+4
-21
@@ -16,19 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
headerFontSize,
|
||||
subheaderFontSize,
|
||||
metricNameFontSize,
|
||||
} from '../sharedControls';
|
||||
import { headerFontSize, subheaderFontSize } from '../sharedControls';
|
||||
|
||||
const headerFontSizes = [16, 20, 30, 48, 60];
|
||||
const sharedFontSizes = [16, 20, 26, 32, 40];
|
||||
|
||||
const metricNameProportionValues =
|
||||
metricNameFontSize.config.options.map(
|
||||
(option: { label: string; value: number }) => option.value,
|
||||
) ?? [];
|
||||
const comparisonFontSizes = [16, 20, 26, 32, 40];
|
||||
|
||||
const headerProportionValues =
|
||||
headerFontSize.config.options.map(
|
||||
@@ -49,10 +40,6 @@ const getFontSizeMapping = (
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const metricNameFontSizesMapping = getFontSizeMapping(
|
||||
metricNameProportionValues,
|
||||
sharedFontSizes,
|
||||
);
|
||||
const headerFontSizesMapping = getFontSizeMapping(
|
||||
headerProportionValues,
|
||||
headerFontSizes,
|
||||
@@ -60,17 +47,13 @@ const headerFontSizesMapping = getFontSizeMapping(
|
||||
|
||||
const comparisonFontSizesMapping = getFontSizeMapping(
|
||||
subheaderProportionValues,
|
||||
sharedFontSizes,
|
||||
comparisonFontSizes,
|
||||
);
|
||||
|
||||
export const getMetricNameFontSize = (proportionValue: number) =>
|
||||
metricNameFontSizesMapping[proportionValue] ??
|
||||
sharedFontSizes[sharedFontSizes.length - 1];
|
||||
|
||||
export const getHeaderFontSize = (proportionValue: number) =>
|
||||
headerFontSizesMapping[proportionValue] ??
|
||||
headerFontSizes[headerFontSizes.length - 1];
|
||||
|
||||
export const getComparisonFontSize = (proportionValue: number) =>
|
||||
comparisonFontSizesMapping[proportionValue] ??
|
||||
sharedFontSizes[sharedFontSizes.length - 1];
|
||||
comparisonFontSizes[comparisonFontSizes.length - 1];
|
||||
|
||||
-4
@@ -28,8 +28,6 @@ import {
|
||||
headerFontSize,
|
||||
subtitleFontSize,
|
||||
subtitleControl,
|
||||
showMetricNameControl,
|
||||
metricNameFontSizeWithVisibility,
|
||||
} from '../sharedControls';
|
||||
|
||||
export default {
|
||||
@@ -46,8 +44,6 @@ export default {
|
||||
[headerFontSize],
|
||||
[subtitleControl],
|
||||
[subtitleFontSize],
|
||||
[showMetricNameControl],
|
||||
[metricNameFontSizeWithVisibility],
|
||||
['y_axis_format'],
|
||||
['currency_format'],
|
||||
[
|
||||
|
||||
@@ -39,7 +39,6 @@ const metadata = {
|
||||
tags: [
|
||||
t('Additive'),
|
||||
t('Business'),
|
||||
t('ECharts'),
|
||||
t('Legacy'),
|
||||
t('Percentages'),
|
||||
t('Featured'),
|
||||
|
||||
-1
@@ -36,7 +36,6 @@ jest.mock('@superset-ui/core', () => ({
|
||||
jest.mock('../utils', () => ({
|
||||
getDateFormatter: jest.fn(() => (v: any) => `${v}pm`),
|
||||
parseMetricValue: jest.fn(val => Number(val)),
|
||||
getOriginalLabel: jest.fn((metric, metrics) => metric),
|
||||
}));
|
||||
|
||||
describe('BigNumberTotal transformProps', () => {
|
||||
|
||||
+3
-9
@@ -29,7 +29,7 @@ import {
|
||||
getValueFormatter,
|
||||
} from '@superset-ui/core';
|
||||
import { BigNumberTotalChartProps, BigNumberVizProps } from '../types';
|
||||
import { getDateFormatter, getOriginalLabel, parseMetricValue } from '../utils';
|
||||
import { getDateFormatter, parseMetricValue } from '../utils';
|
||||
import { Refs } from '../../types';
|
||||
|
||||
export default function transformProps(
|
||||
@@ -45,7 +45,6 @@ export default function transformProps(
|
||||
datasource: { currencyFormats = {}, columnFormats = {} },
|
||||
} = chartProps;
|
||||
const {
|
||||
metricNameFontSize,
|
||||
headerFontSize,
|
||||
metric = 'value',
|
||||
subtitle,
|
||||
@@ -59,12 +58,9 @@ export default function transformProps(
|
||||
subheaderFontSize,
|
||||
} = formData;
|
||||
const refs: Refs = {};
|
||||
const { data = [], coltypes = [] } = queriesData[0] || {};
|
||||
const { data = [], coltypes = [] } = queriesData[0];
|
||||
const granularity = extractTimegrain(rawFormData as QueryFormData);
|
||||
const metrics = chartProps.datasource?.metrics || [];
|
||||
const originalLabel = getOriginalLabel(metric, metrics);
|
||||
const metricName = getMetricLabel(metric);
|
||||
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
||||
const formattedSubtitle = subtitle?.trim() ? subtitle : subheader || '';
|
||||
const formattedSubtitleFontSize = subtitle?.trim()
|
||||
? (subtitleFontSize ?? 1)
|
||||
@@ -107,6 +103,7 @@ export default function transformProps(
|
||||
const colorThresholdFormatters =
|
||||
getColorFormatters(conditionalFormatting, data, false) ??
|
||||
defaultColorFormatters;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
@@ -119,8 +116,5 @@ export default function transformProps(
|
||||
onContextMenu,
|
||||
refs,
|
||||
colorThresholdFormatters,
|
||||
metricName: originalLabel,
|
||||
showMetricName,
|
||||
metricNameFontSize,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { PureComponent, MouseEvent, createRef } from 'react';
|
||||
import { PureComponent, MouseEvent } from 'react';
|
||||
import {
|
||||
t,
|
||||
getNumberFormatter,
|
||||
@@ -35,7 +35,6 @@ const defaultNumberFormatter = getNumberFormatter();
|
||||
|
||||
const PROPORTION = {
|
||||
// text size: proportion of the chart container sans trendline
|
||||
METRIC_NAME: 0.125,
|
||||
KICKER: 0.1,
|
||||
HEADER: 0.3,
|
||||
SUBHEADER: 0.125,
|
||||
@@ -43,20 +42,13 @@ const PROPORTION = {
|
||||
TRENDLINE: 0.3,
|
||||
};
|
||||
|
||||
type BigNumberVisState = {
|
||||
elementsRendered: boolean;
|
||||
recalculateTrigger: boolean;
|
||||
};
|
||||
|
||||
class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
headerFormatter: defaultNumberFormatter,
|
||||
formatTime: getTimeFormatter(SMART_DATE_VERBOSE_ID),
|
||||
headerFontSize: PROPORTION.HEADER,
|
||||
kickerFontSize: PROPORTION.KICKER,
|
||||
metricNameFontSize: PROPORTION.METRIC_NAME,
|
||||
showMetricName: true,
|
||||
mainColor: BRAND_COLOR,
|
||||
showTimestamp: false,
|
||||
showTrendLine: false,
|
||||
@@ -66,40 +58,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
timeRangeFixed: false,
|
||||
};
|
||||
|
||||
// Create refs for each component to measure heights
|
||||
metricNameRef = createRef<HTMLDivElement>();
|
||||
|
||||
kickerRef = createRef<HTMLDivElement>();
|
||||
|
||||
headerRef = createRef<HTMLDivElement>();
|
||||
|
||||
subheaderRef = createRef<HTMLDivElement>();
|
||||
|
||||
subtitleRef = createRef<HTMLDivElement>();
|
||||
|
||||
state = {
|
||||
elementsRendered: false,
|
||||
recalculateTrigger: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// Wait for elements to render and then calculate heights
|
||||
setTimeout(() => {
|
||||
this.setState({ elementsRendered: true });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: BigNumberVizProps) {
|
||||
if (
|
||||
prevProps.height !== this.props.height ||
|
||||
prevProps.showTrendLine !== this.props.showTrendLine
|
||||
) {
|
||||
this.setState(prevState => ({
|
||||
recalculateTrigger: !prevState.recalculateTrigger,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
getClassName() {
|
||||
const { className, showTrendLine, bigNumberFallback } = this.props;
|
||||
const names = `superset-legacy-chart-big-number ${className} ${
|
||||
@@ -134,37 +92,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
);
|
||||
}
|
||||
|
||||
renderMetricName(maxHeight: number) {
|
||||
const { metricName, width, showMetricName } = this.props;
|
||||
if (!showMetricName || !metricName) return null;
|
||||
|
||||
const text = metricName;
|
||||
|
||||
const container = this.createTemporaryContainer();
|
||||
document.body.append(container);
|
||||
const fontSize = computeMaxFontSize({
|
||||
text,
|
||||
maxWidth: width,
|
||||
maxHeight,
|
||||
className: 'metric-name',
|
||||
container,
|
||||
});
|
||||
container.remove();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.metricNameRef}
|
||||
className="metric-name"
|
||||
style={{
|
||||
fontSize,
|
||||
height: 'auto',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderKicker(maxHeight: number) {
|
||||
const { timestamp, showTimestamp, formatTime, width } = this.props;
|
||||
if (
|
||||
@@ -191,7 +118,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.kickerRef}
|
||||
className="kicker"
|
||||
style={{
|
||||
fontSize,
|
||||
@@ -247,7 +173,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.headerRef}
|
||||
className="header-line"
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -286,7 +211,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.subheaderRef}
|
||||
className="subheader-line"
|
||||
style={{
|
||||
fontSize,
|
||||
@@ -332,7 +256,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={this.subtitleRef}
|
||||
className="subtitle-line subheader-line"
|
||||
style={{
|
||||
fontSize: `${fontSize}px`,
|
||||
@@ -393,35 +316,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
);
|
||||
}
|
||||
|
||||
getTotalElementsHeight() {
|
||||
const marginPerElement = 8; // theme.gridUnit = 4, so margin-bottom = 8px
|
||||
|
||||
const refs = [
|
||||
this.metricNameRef,
|
||||
this.kickerRef,
|
||||
this.headerRef,
|
||||
this.subheaderRef,
|
||||
this.subtitleRef,
|
||||
];
|
||||
|
||||
// Filter refs to only those with a current element
|
||||
const visibleRefs = refs.filter(ref => ref.current);
|
||||
|
||||
const totalHeight = visibleRefs.reduce((sum, ref, index) => {
|
||||
const height = ref.current?.offsetHeight || 0;
|
||||
const margin = index < visibleRefs.length - 1 ? marginPerElement : 0;
|
||||
return sum + height + margin;
|
||||
}, 0);
|
||||
|
||||
return totalHeight;
|
||||
}
|
||||
|
||||
shouldApplyOverflow(availableHeight: number) {
|
||||
if (!this.state.elementsRendered) return false;
|
||||
const totalHeight = this.getTotalElementsHeight();
|
||||
return totalHeight > availableHeight;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
showTrendLine,
|
||||
@@ -429,7 +323,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
kickerFontSize,
|
||||
headerFontSize,
|
||||
subtitleFontSize,
|
||||
metricNameFontSize,
|
||||
subheaderFontSize,
|
||||
} = this.props;
|
||||
const className = this.getClassName();
|
||||
@@ -437,31 +330,11 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
if (showTrendLine) {
|
||||
const chartHeight = Math.floor(PROPORTION.TRENDLINE * height);
|
||||
const allTextHeight = height - chartHeight;
|
||||
const shouldApplyOverflow = this.shouldApplyOverflow(allTextHeight);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
className="text-container"
|
||||
style={{
|
||||
height: allTextHeight,
|
||||
...(shouldApplyOverflow
|
||||
? {
|
||||
display: 'block',
|
||||
boxSizing: 'border-box',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<div className="text-container" style={{ height: allTextHeight }}>
|
||||
{this.renderFallbackWarning()}
|
||||
{this.renderMetricName(
|
||||
Math.ceil(
|
||||
(metricNameFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
|
||||
),
|
||||
)}
|
||||
{this.renderKicker(
|
||||
Math.ceil(
|
||||
(kickerFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
|
||||
@@ -483,33 +356,16 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const shouldApplyOverflow = this.shouldApplyOverflow(height);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
height,
|
||||
...(shouldApplyOverflow
|
||||
? {
|
||||
display: 'block',
|
||||
boxSizing: 'border-box',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<div className="text-container">
|
||||
{this.renderFallbackWarning()}
|
||||
{this.renderMetricName((metricNameFontSize || 0) * height)}
|
||||
{this.renderKicker((kickerFontSize || 0) * height)}
|
||||
{this.renderHeader(Math.ceil(headerFontSize * height))}
|
||||
{this.rendermetricComparisonSummary(
|
||||
Math.ceil(subheaderFontSize * height),
|
||||
)}
|
||||
{this.renderSubtitle(Math.ceil(subtitleFontSize * height))}
|
||||
</div>
|
||||
<div className={className} style={{ height }}>
|
||||
{this.renderFallbackWarning()}
|
||||
{this.renderKicker((kickerFontSize || 0) * height)}
|
||||
{this.renderHeader(Math.ceil(headerFontSize * height))}
|
||||
{this.rendermetricComparisonSummary(
|
||||
Math.ceil(subheaderFontSize * height),
|
||||
)}
|
||||
{this.renderSubtitle(Math.ceil(subtitleFontSize * height))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -544,12 +400,7 @@ export default styled(BigNumberVis)`
|
||||
|
||||
.kicker {
|
||||
line-height: 1em;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
|
||||
.metric-name {
|
||||
line-height: 1em;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
.header-line {
|
||||
@@ -565,12 +416,12 @@ export default styled(BigNumberVis)`
|
||||
|
||||
.subheader-line {
|
||||
line-height: 1em;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.subtitle-line {
|
||||
line-height: 1em;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&.is-fallback-value {
|
||||
|
||||
-4
@@ -31,8 +31,6 @@ import {
|
||||
subheaderFontSize,
|
||||
subtitleFontSize,
|
||||
subtitleControl,
|
||||
showMetricNameControl,
|
||||
metricNameFontSizeWithVisibility,
|
||||
} from '../sharedControls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -143,8 +141,6 @@ const config: ControlPanelConfig = {
|
||||
[subheaderFontSize],
|
||||
[subtitleControl],
|
||||
[subtitleFontSize],
|
||||
[showMetricNameControl],
|
||||
[metricNameFontSizeWithVisibility],
|
||||
['y_axis_format'],
|
||||
['currency_format'],
|
||||
[
|
||||
|
||||
-1
@@ -37,7 +37,6 @@ const metadata = {
|
||||
name: t('Big Number with Trendline'),
|
||||
tags: [
|
||||
t('Advanced-Analytics'),
|
||||
t('ECharts'),
|
||||
t('Line'),
|
||||
t('Percentages'),
|
||||
t('Featured'),
|
||||
|
||||
-1
@@ -39,7 +39,6 @@ jest.mock('@superset-ui/core', () => ({
|
||||
jest.mock('../utils', () => ({
|
||||
getDateFormatter: jest.fn(() => (v: any) => `${v}pm`),
|
||||
parseMetricValue: jest.fn(val => Number(val)),
|
||||
getOriginalLabel: jest.fn((metric, metrics) => metric),
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/tooltip', () => ({
|
||||
|
||||
+1
-8
@@ -35,7 +35,7 @@ import {
|
||||
BigNumberWithTrendlineChartProps,
|
||||
TimeSeriesDatum,
|
||||
} from '../types';
|
||||
import { getDateFormatter, parseMetricValue, getOriginalLabel } from '../utils';
|
||||
import { getDateFormatter, parseMetricValue } from '../utils';
|
||||
import { getDefaultTooltip } from '../../utils/tooltip';
|
||||
import { Refs } from '../../types';
|
||||
|
||||
@@ -62,7 +62,6 @@ export default function transformProps(
|
||||
compareLag: compareLag_,
|
||||
compareSuffix = '',
|
||||
timeFormat,
|
||||
metricNameFontSize,
|
||||
headerFontSize,
|
||||
metric = 'value',
|
||||
showTimestamp,
|
||||
@@ -97,9 +96,6 @@ export default function transformProps(
|
||||
const aggregatedData = hasAggregatedData ? aggregatedQueryData.data[0] : null;
|
||||
const refs: Refs = {};
|
||||
const metricName = getMetricLabel(metric);
|
||||
const metrics = chartProps.datasource?.metrics || [];
|
||||
const originalLabel = getOriginalLabel(metric, metrics);
|
||||
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
||||
const compareLag = Number(compareLag_) || 0;
|
||||
let formattedSubheader = subheader;
|
||||
|
||||
@@ -307,9 +303,6 @@ export default function transformProps(
|
||||
headerFormatter,
|
||||
formatTime,
|
||||
formData,
|
||||
metricName: originalLabel,
|
||||
showMetricName,
|
||||
metricNameFontSize,
|
||||
headerFontSize,
|
||||
subtitleFontSize,
|
||||
subtitle,
|
||||
|
||||
@@ -21,68 +21,106 @@
|
||||
import { t } from '@superset-ui/core';
|
||||
import { CustomControlItem } from '@superset-ui/chart-controls';
|
||||
|
||||
const FONT_SIZE_OPTIONS_SMALL = [
|
||||
{ label: t('Tiny'), value: 0.125 },
|
||||
{ label: t('Small'), value: 0.15 },
|
||||
{ label: t('Normal'), value: 0.2 },
|
||||
{ label: t('Large'), value: 0.3 },
|
||||
{ label: t('Huge'), value: 0.4 },
|
||||
];
|
||||
export const headerFontSize: CustomControlItem = {
|
||||
name: 'header_font_size',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Big Number Font Size'),
|
||||
renderTrigger: true,
|
||||
clearable: false,
|
||||
default: 0.4,
|
||||
// Values represent the percentage of space a header should take
|
||||
options: [
|
||||
{
|
||||
label: t('Tiny'),
|
||||
value: 0.2,
|
||||
},
|
||||
{
|
||||
label: t('Small'),
|
||||
value: 0.3,
|
||||
},
|
||||
{
|
||||
label: t('Normal'),
|
||||
value: 0.4,
|
||||
},
|
||||
{
|
||||
label: t('Large'),
|
||||
value: 0.5,
|
||||
},
|
||||
{
|
||||
label: t('Huge'),
|
||||
value: 0.6,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const FONT_SIZE_OPTIONS_LARGE = [
|
||||
{ label: t('Tiny'), value: 0.2 },
|
||||
{ label: t('Small'), value: 0.3 },
|
||||
{ label: t('Normal'), value: 0.4 },
|
||||
{ label: t('Large'), value: 0.5 },
|
||||
{ label: t('Huge'), value: 0.6 },
|
||||
];
|
||||
|
||||
function makeFontSizeControl(
|
||||
name: string,
|
||||
label: string,
|
||||
defaultValue: number,
|
||||
options: { label: string; value: number }[],
|
||||
): CustomControlItem {
|
||||
return {
|
||||
name,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t(label),
|
||||
renderTrigger: true,
|
||||
clearable: false,
|
||||
default: defaultValue,
|
||||
options,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const headerFontSize = makeFontSizeControl(
|
||||
'header_font_size',
|
||||
'Big Number Font Size',
|
||||
0.4,
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
);
|
||||
|
||||
export const subtitleFontSize = makeFontSizeControl(
|
||||
'subtitle_font_size',
|
||||
'Subtitle Font Size',
|
||||
0.15,
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
);
|
||||
|
||||
export const subheaderFontSize = makeFontSizeControl(
|
||||
'subheader_font_size',
|
||||
'Subheader Font Size',
|
||||
0.15,
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
);
|
||||
|
||||
export const metricNameFontSize = makeFontSizeControl(
|
||||
'metric_name_font_size',
|
||||
'Metric Name Font Size',
|
||||
0.15,
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
);
|
||||
export const subtitleFontSize: CustomControlItem = {
|
||||
name: 'subtitle_font_size',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subtitle Font Size'),
|
||||
renderTrigger: true,
|
||||
clearable: false,
|
||||
default: 0.15,
|
||||
// Values represent the percentage of space a subtitle should take
|
||||
options: [
|
||||
{
|
||||
label: t('Tiny'),
|
||||
value: 0.125,
|
||||
},
|
||||
{
|
||||
label: t('Small'),
|
||||
value: 0.15,
|
||||
},
|
||||
{
|
||||
label: t('Normal'),
|
||||
value: 0.2,
|
||||
},
|
||||
{
|
||||
label: t('Large'),
|
||||
value: 0.3,
|
||||
},
|
||||
{
|
||||
label: t('Huge'),
|
||||
value: 0.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
export const subheaderFontSize: CustomControlItem = {
|
||||
name: 'subheader_font_size',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subheader Font Size'),
|
||||
renderTrigger: true,
|
||||
clearable: false,
|
||||
default: 0.15,
|
||||
// Values represent the percentage of space a subheader should take
|
||||
options: [
|
||||
{
|
||||
label: t('Tiny'),
|
||||
value: 0.125,
|
||||
},
|
||||
{
|
||||
label: t('Small'),
|
||||
value: 0.15,
|
||||
},
|
||||
{
|
||||
label: t('Normal'),
|
||||
value: 0.2,
|
||||
},
|
||||
{
|
||||
label: t('Large'),
|
||||
value: 0.3,
|
||||
},
|
||||
{
|
||||
label: t('Huge'),
|
||||
value: 0.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const subtitleControl: CustomControlItem = {
|
||||
name: 'subtitle',
|
||||
@@ -93,23 +131,3 @@ export const subtitleControl: CustomControlItem = {
|
||||
description: t('Description text that shows up below your Big Number'),
|
||||
},
|
||||
};
|
||||
|
||||
export const showMetricNameControl: CustomControlItem = {
|
||||
name: 'show_metric_name',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Metric Name'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t('Whether to display the metric name'),
|
||||
},
|
||||
};
|
||||
|
||||
export const metricNameFontSizeWithVisibility: CustomControlItem = {
|
||||
...metricNameFontSize,
|
||||
config: {
|
||||
...metricNameFontSize.config,
|
||||
visibility: ({ controls }) => controls?.show_metric_name?.value === true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -75,10 +75,6 @@ export type BigNumberVizProps = {
|
||||
bigNumberFallback?: TimeSeriesDatum;
|
||||
headerFormatter: ValueFormatter | TimeFormatter;
|
||||
formatTime?: TimeFormatter;
|
||||
metricName?: string;
|
||||
friendlyMetricName?: string;
|
||||
metricNameFontSize?: number;
|
||||
showMetricName?: boolean;
|
||||
headerFontSize: number;
|
||||
kickerFontSize?: number;
|
||||
subheader?: string;
|
||||
|
||||
@@ -22,10 +22,6 @@ import utc from 'dayjs/plugin/utc';
|
||||
import {
|
||||
getTimeFormatter,
|
||||
getTimeFormatterForGranularity,
|
||||
isAdhocMetricSimple,
|
||||
isSavedMetric,
|
||||
Metric,
|
||||
QueryFormMetric,
|
||||
SMART_DATE_ID,
|
||||
TimeGranularity,
|
||||
} from '@superset-ui/core';
|
||||
@@ -51,43 +47,3 @@ export const getDateFormatter = (
|
||||
timeFormat === SMART_DATE_ID
|
||||
? getTimeFormatterForGranularity(granularity)
|
||||
: getTimeFormatter(timeFormat ?? fallbackFormat);
|
||||
|
||||
export function getOriginalLabel(
|
||||
metric: QueryFormMetric,
|
||||
metrics: Metric[] = [],
|
||||
): string {
|
||||
const metricLabel = typeof metric === 'string' ? metric : metric.label || '';
|
||||
|
||||
if (isSavedMetric(metric)) {
|
||||
const metricEntry = metrics.find(m => m.metric_name === metric);
|
||||
return (
|
||||
metricEntry?.verbose_name ||
|
||||
metricEntry?.metric_name ||
|
||||
metric ||
|
||||
'Unknown Metric'
|
||||
);
|
||||
}
|
||||
|
||||
if (isAdhocMetricSimple(metric)) {
|
||||
const column = metric.column || {};
|
||||
const columnName = column.column_name || 'unknown_column';
|
||||
const verboseName = column.verbose_name || columnName;
|
||||
const aggregate = metric.aggregate || 'UNKNOWN';
|
||||
return metric.hasCustomLabel && metric.label
|
||||
? metric.label
|
||||
: `${aggregate}(${verboseName})`;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof metric === 'object' &&
|
||||
'expressionType' in metric &&
|
||||
metric.expressionType === 'SQL' &&
|
||||
'sqlExpression' in metric
|
||||
) {
|
||||
return metric.hasCustomLabel && metric.label
|
||||
? metric.label
|
||||
: metricLabel || 'Custom Metric';
|
||||
}
|
||||
|
||||
return metricLabel || 'Unknown Metric';
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export default class EchartsSankeyChartPlugin extends ChartPlugin<
|
||||
),
|
||||
exampleGallery: [{ url: example1 }, { url: example2 }],
|
||||
name: t('Sankey Chart'),
|
||||
tags: [t('Directional'), t('ECharts'), t('Distribution'), t('Flow')],
|
||||
tags: [t('Directional'), t('Distribution'), t('Flow')],
|
||||
thumbnail,
|
||||
}),
|
||||
transformProps,
|
||||
|
||||
@@ -58,7 +58,6 @@ export default class EchartsTimeseriesChartPlugin extends EchartsChartPlugin<
|
||||
name: t('Generic Chart'),
|
||||
tags: [
|
||||
t('Advanced-Analytics'),
|
||||
t('ECharts'),
|
||||
t('Line'),
|
||||
t('Predictive'),
|
||||
t('Time'),
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
@@ -25,7 +24,6 @@ import {
|
||||
MutableRefObject,
|
||||
CSSProperties,
|
||||
DragEvent,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
@@ -41,9 +39,8 @@ import {
|
||||
Row,
|
||||
} from 'react-table';
|
||||
import { matchSorter, rankings } from 'match-sorter';
|
||||
import { styled, typedMemo, usePrevious } from '@superset-ui/core';
|
||||
import { typedMemo, usePrevious } from '@superset-ui/core';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Space } from 'antd';
|
||||
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
|
||||
import SelectPageSize, {
|
||||
SelectPageSizeProps,
|
||||
@@ -53,8 +50,6 @@ import SimplePagination from './components/Pagination';
|
||||
import useSticky from './hooks/useSticky';
|
||||
import { PAGE_SIZE_OPTIONS } from '../consts';
|
||||
import { sortAlphanumericCaseInsensitive } from './utils/sortAlphanumericCaseInsensitive';
|
||||
import { SearchOption, SortByItem } from '../types';
|
||||
import SearchSelectDropdown from './components/SearchSelectDropdown';
|
||||
|
||||
export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
tableClassName?: string;
|
||||
@@ -67,12 +62,7 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
height?: string | number;
|
||||
serverPagination?: boolean;
|
||||
onServerPaginationChange: (pageNumber: number, pageSize: number) => void;
|
||||
serverPaginationData: {
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
sortBy?: SortByItem[];
|
||||
searchColumn?: string;
|
||||
};
|
||||
serverPaginationData: { pageSize?: number; currentPage?: number };
|
||||
pageSize?: number;
|
||||
noResults?: string | ((filterString: string) => ReactNode);
|
||||
sticky?: boolean;
|
||||
@@ -81,14 +71,6 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
onColumnOrderChange: () => void;
|
||||
renderGroupingHeaders?: () => JSX.Element;
|
||||
renderTimeComparisonDropdown?: () => JSX.Element;
|
||||
handleSortByChange: (sortBy: SortByItem[]) => void;
|
||||
sortByFromParent: SortByItem[];
|
||||
manualSearch?: boolean;
|
||||
onSearchChange?: (searchText: string) => void;
|
||||
initialSearchText?: string;
|
||||
searchInputId?: string;
|
||||
onSearchColChange: (searchCol: string) => void;
|
||||
searchOptions: SearchOption[];
|
||||
}
|
||||
|
||||
export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
|
||||
@@ -99,20 +81,6 @@ const sortTypes = {
|
||||
alphanumeric: sortAlphanumericCaseInsensitive,
|
||||
};
|
||||
|
||||
const StyledSpace = styled(Space)`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.search-select-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-by-label {
|
||||
align-self: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
// Be sure to pass our updateMyData and the skipReset option
|
||||
export default typedMemo(function DataTable<D extends object>({
|
||||
tableClassName,
|
||||
@@ -137,14 +105,6 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
onColumnOrderChange,
|
||||
renderGroupingHeaders,
|
||||
renderTimeComparisonDropdown,
|
||||
handleSortByChange,
|
||||
sortByFromParent = [],
|
||||
manualSearch = false,
|
||||
onSearchChange,
|
||||
initialSearchText,
|
||||
searchInputId,
|
||||
onSearchColChange,
|
||||
searchOptions,
|
||||
...moreUseTableOptions
|
||||
}: DataTableProps<D>): JSX.Element {
|
||||
const tableHooks: PluginHook<D>[] = [
|
||||
@@ -155,7 +115,6 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
doSticky ? useSticky : [],
|
||||
hooks || [],
|
||||
].flat();
|
||||
|
||||
const columnNames = Object.keys(data?.[0] || {});
|
||||
const previousColumnNames = usePrevious(columnNames);
|
||||
const resultsSize = serverPagination ? rowCount : data.length;
|
||||
@@ -168,8 +127,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
...initialState_,
|
||||
// zero length means all pages, the `usePagination` plugin does not
|
||||
// understand pageSize = 0
|
||||
// sortBy: sortByRef.current,
|
||||
sortBy: serverPagination ? sortByFromParent : sortByRef.current,
|
||||
sortBy: sortByRef.current,
|
||||
pageSize: initialPageSize > 0 ? initialPageSize : resultsSize || 10,
|
||||
};
|
||||
const defaultWrapperRef = useRef<HTMLDivElement>(null);
|
||||
@@ -230,13 +188,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
wrapStickyTable,
|
||||
setColumnOrder,
|
||||
allColumns,
|
||||
state: {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
globalFilter: filterValue,
|
||||
sticky = {},
|
||||
sortBy,
|
||||
},
|
||||
state: { pageIndex, pageSize, globalFilter: filterValue, sticky = {} },
|
||||
} = useTable<D>(
|
||||
{
|
||||
columns,
|
||||
@@ -246,46 +198,10 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
globalFilter: defaultGlobalFilter,
|
||||
sortTypes,
|
||||
autoResetSortBy: !isEqual(columnNames, previousColumnNames),
|
||||
manualSortBy: !!serverPagination,
|
||||
...moreUseTableOptions,
|
||||
},
|
||||
...tableHooks,
|
||||
);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(query: string) => {
|
||||
if (manualSearch && onSearchChange) {
|
||||
onSearchChange(query);
|
||||
} else {
|
||||
setGlobalFilter(query);
|
||||
}
|
||||
},
|
||||
[manualSearch, onSearchChange, setGlobalFilter],
|
||||
);
|
||||
|
||||
// updating the sort by to the own State of table viz
|
||||
useEffect(() => {
|
||||
const serverSortBy = serverPaginationData?.sortBy || [];
|
||||
|
||||
if (serverPagination && !isEqual(sortBy, serverSortBy)) {
|
||||
if (Array.isArray(sortBy) && sortBy.length > 0) {
|
||||
const [sortByItem] = sortBy;
|
||||
const matchingColumn = columns.find(col => col?.id === sortByItem?.id);
|
||||
|
||||
if (matchingColumn && 'columnKey' in matchingColumn) {
|
||||
const sortByWithColumnKey: SortByItem = {
|
||||
...sortByItem,
|
||||
key: (matchingColumn as { columnKey: string }).columnKey,
|
||||
};
|
||||
|
||||
handleSortByChange([sortByWithColumnKey]);
|
||||
}
|
||||
} else {
|
||||
handleSortByChange([]);
|
||||
}
|
||||
}
|
||||
}, [sortBy]);
|
||||
|
||||
// make setPageSize accept 0
|
||||
const setPageSize = (size: number) => {
|
||||
if (serverPagination) {
|
||||
@@ -439,7 +355,6 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
resultOnPageChange = (pageNumber: number) =>
|
||||
onServerPaginationChange(pageNumber, serverPageSize);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
@@ -466,31 +381,16 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
) : null}
|
||||
</div>
|
||||
{searchInput ? (
|
||||
<StyledSpace className="col-sm-6">
|
||||
{serverPagination && (
|
||||
<div className="search-select-container">
|
||||
<span className="search-by-label">Search by: </span>
|
||||
<SearchSelectDropdown
|
||||
searchOptions={searchOptions}
|
||||
value={serverPaginationData?.searchColumn || ''}
|
||||
onChange={onSearchColChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="col-sm-6">
|
||||
<GlobalFilter<D>
|
||||
searchInput={
|
||||
typeof searchInput === 'boolean' ? undefined : searchInput
|
||||
}
|
||||
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||
setGlobalFilter={
|
||||
manualSearch ? handleSearchChange : setGlobalFilter
|
||||
}
|
||||
filterValue={manualSearch ? initialSearchText : filterValue}
|
||||
id={searchInputId}
|
||||
serverPagination={!!serverPagination}
|
||||
rowCount={rowCount}
|
||||
setGlobalFilter={setGlobalFilter}
|
||||
filterValue={filterValue}
|
||||
/>
|
||||
</StyledSpace>
|
||||
</div>
|
||||
) : null}
|
||||
{renderTimeComparisonDropdown ? (
|
||||
<div
|
||||
|
||||
+8
-54
@@ -16,13 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
memo,
|
||||
ComponentType,
|
||||
ChangeEventHandler,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { memo, ComponentType, ChangeEventHandler } from 'react';
|
||||
import { Row, FilterValue } from 'react-table';
|
||||
import useAsyncState from '../utils/useAsyncState';
|
||||
|
||||
@@ -30,12 +24,8 @@ export interface SearchInputProps {
|
||||
count: number;
|
||||
value: string;
|
||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||
onBlur?: () => void;
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const isSearchFocused = new Map();
|
||||
|
||||
export interface GlobalFilterProps<D extends object> {
|
||||
preGlobalFilteredRows: Row<D>[];
|
||||
// filter value cannot be `undefined` otherwise React will report component
|
||||
@@ -43,28 +33,17 @@ export interface GlobalFilterProps<D extends object> {
|
||||
filterValue: string;
|
||||
setGlobalFilter: (filterValue: FilterValue) => void;
|
||||
searchInput?: ComponentType<SearchInputProps>;
|
||||
id?: string;
|
||||
serverPagination: boolean;
|
||||
rowCount: number;
|
||||
}
|
||||
|
||||
function DefaultSearchInput({
|
||||
count,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
inputRef,
|
||||
}: SearchInputProps) {
|
||||
function DefaultSearchInput({ count, value, onChange }: SearchInputProps) {
|
||||
return (
|
||||
<span className="dt-global-filter">
|
||||
Search{' '}
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="form-control input-sm"
|
||||
placeholder={`${count} records...`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
@@ -77,13 +56,8 @@ export default (memo as <T>(fn: T) => T)(function GlobalFilter<
|
||||
filterValue = '',
|
||||
searchInput,
|
||||
setGlobalFilter,
|
||||
id = '',
|
||||
serverPagination,
|
||||
rowCount,
|
||||
}: GlobalFilterProps<D>) {
|
||||
const count = serverPagination ? rowCount : preGlobalFilteredRows.length;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const count = preGlobalFilteredRows.length;
|
||||
const [value, setValue] = useAsyncState(
|
||||
filterValue,
|
||||
(newValue: string) => {
|
||||
@@ -92,37 +66,17 @@ export default (memo as <T>(fn: T) => T)(function GlobalFilter<
|
||||
200,
|
||||
);
|
||||
|
||||
// Preserve focus during server-side filtering to maintain a better user experience
|
||||
useEffect(() => {
|
||||
if (
|
||||
serverPagination &&
|
||||
isSearchFocused.get(id) &&
|
||||
document.activeElement !== inputRef.current
|
||||
) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [value, serverPagination]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
e.preventDefault();
|
||||
isSearchFocused.set(id, true);
|
||||
setValue(target.value);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
isSearchFocused.set(id, false);
|
||||
};
|
||||
|
||||
const SearchInput = searchInput || DefaultSearchInput;
|
||||
|
||||
return (
|
||||
<SearchInput
|
||||
count={count}
|
||||
value={value}
|
||||
inputRef={inputRef}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onChange={e => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
e.preventDefault();
|
||||
setValue(target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { Select } from 'antd';
|
||||
import { SearchOption } from '../../types';
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
width: 120px;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
interface SearchSelectDropdownProps {
|
||||
/** The currently selected search column value */
|
||||
value?: string;
|
||||
/** Callback triggered when a new search column is selected */
|
||||
onChange: (searchCol: string) => void;
|
||||
/** Available search column options to populate the dropdown */
|
||||
searchOptions: SearchOption[];
|
||||
}
|
||||
|
||||
function SearchSelectDropdown({
|
||||
value,
|
||||
onChange,
|
||||
searchOptions,
|
||||
}: SearchSelectDropdownProps) {
|
||||
return (
|
||||
<StyledSelect
|
||||
className="search-select"
|
||||
value={value || (searchOptions?.[0]?.value ?? '')}
|
||||
options={searchOptions}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchSelectDropdown;
|
||||
-8
@@ -115,11 +115,3 @@ declare module 'react-table' {
|
||||
extends UseTableHooks<D>,
|
||||
UseSortByHooks<D> {}
|
||||
}
|
||||
|
||||
interface TableOwnState {
|
||||
currentPage?: number;
|
||||
pageSize?: number;
|
||||
sortColumn?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
searchText?: string;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
|
||||
import { SetDataMaskHook } from '@superset-ui/core';
|
||||
import { TableOwnState } from '../types/react-table';
|
||||
|
||||
export const updateExternalFormData = (
|
||||
setDataMask: SetDataMaskHook = () => {},
|
||||
@@ -31,11 +30,3 @@ export const updateExternalFormData = (
|
||||
pageSize,
|
||||
},
|
||||
});
|
||||
|
||||
export const updateTableOwnState = (
|
||||
setDataMask: SetDataMaskHook = () => {},
|
||||
modifiedOwnState: TableOwnState,
|
||||
) =>
|
||||
setDataMask({
|
||||
ownState: modifiedOwnState,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
useState,
|
||||
MouseEvent,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
@@ -62,12 +61,10 @@ import {
|
||||
PlusCircleOutlined,
|
||||
TableOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { debounce, isEmpty, isEqual } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
ColorSchemeEnum,
|
||||
DataColumnMeta,
|
||||
SearchOption,
|
||||
SortByItem,
|
||||
TableChartTransformedProps,
|
||||
} from './types';
|
||||
import DataTable, {
|
||||
@@ -80,7 +77,7 @@ import DataTable, {
|
||||
import Styles from './Styles';
|
||||
import { formatColumnValue } from './utils/formatValue';
|
||||
import { PAGE_SIZE_OPTIONS } from './consts';
|
||||
import { updateTableOwnState } from './DataTable/utils/externalAPIs';
|
||||
import { updateExternalFormData } from './DataTable/utils/externalAPIs';
|
||||
import getScrollBarSize from './DataTable/utils/getScrollBarSize';
|
||||
|
||||
type ValueRange = [number, number];
|
||||
@@ -179,26 +176,20 @@ function SortIcon<D extends object>({ column }: { column: ColumnInstance<D> }) {
|
||||
return sortIcon;
|
||||
}
|
||||
|
||||
const SearchInput = ({
|
||||
count,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
inputRef,
|
||||
}: SearchInputProps) => (
|
||||
<span className="dt-global-filter">
|
||||
{t('Search')}{' '}
|
||||
<input
|
||||
ref={inputRef}
|
||||
aria-label={t('Search %s records', count)}
|
||||
className="form-control input-sm"
|
||||
placeholder={tn('search.num_records', count)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
function SearchInput({ count, value, onChange }: SearchInputProps) {
|
||||
return (
|
||||
<span className="dt-global-filter">
|
||||
{t('Search')}{' '}
|
||||
<input
|
||||
aria-label={t('Search %s records', count)}
|
||||
className="form-control input-sm"
|
||||
placeholder={tn('search.num_records', count)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectPageSize({
|
||||
options,
|
||||
@@ -276,9 +267,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
isUsingTimeComparison,
|
||||
basicColorFormatters,
|
||||
basicColorColumnFormatters,
|
||||
hasServerPageLengthChanged,
|
||||
serverPageLength,
|
||||
slice_id,
|
||||
} = props;
|
||||
const comparisonColumns = [
|
||||
{ key: 'all', label: t('Display all') },
|
||||
@@ -691,12 +679,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
);
|
||||
|
||||
const getColumnConfigs = useCallback(
|
||||
(
|
||||
column: DataColumnMeta,
|
||||
i: number,
|
||||
): ColumnWithLooseAccessor<D> & {
|
||||
columnKey: string;
|
||||
} => {
|
||||
(column: DataColumnMeta, i: number): ColumnWithLooseAccessor<D> => {
|
||||
const {
|
||||
key,
|
||||
label: originalLabel,
|
||||
@@ -783,7 +766,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
// must use custom accessor to allow `.` in column names
|
||||
// typing is incorrect in current version of `@types/react-table`
|
||||
// so we ask TS not to check.
|
||||
columnKey: key,
|
||||
accessor: ((datum: D) => datum[key]) as never,
|
||||
Cell: ({ value, row }: { value: DataRecordValue; row: Row<D> }) => {
|
||||
const [isHtml, text] = formatColumnValue(column, value);
|
||||
@@ -1076,50 +1058,13 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
[visibleColumnsMeta, getColumnConfigs],
|
||||
);
|
||||
|
||||
const [searchOptions, setSearchOptions] = useState<SearchOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const options = (
|
||||
columns as unknown as ColumnWithLooseAccessor &
|
||||
{
|
||||
columnKey: string;
|
||||
sortType?: string;
|
||||
}[]
|
||||
)
|
||||
.filter(col => col?.sortType === 'alphanumeric')
|
||||
.map(column => ({
|
||||
value: column.columnKey,
|
||||
label: column.columnKey,
|
||||
}));
|
||||
|
||||
if (!isEqual(options, searchOptions)) {
|
||||
setSearchOptions(options || []);
|
||||
}
|
||||
}, [columns]);
|
||||
|
||||
const handleServerPaginationChange = useCallback(
|
||||
(pageNumber: number, pageSize: number) => {
|
||||
const modifiedOwnState = {
|
||||
...serverPaginationData,
|
||||
currentPage: pageNumber,
|
||||
pageSize,
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
updateExternalFormData(setDataMask, pageNumber, pageSize);
|
||||
},
|
||||
[setDataMask],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasServerPageLengthChanged) {
|
||||
const modifiedOwnState = {
|
||||
...serverPaginationData,
|
||||
currentPage: 0,
|
||||
pageSize: serverPageLength,
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSizeChange = useCallback(
|
||||
({ width, height }: { width: number; height: number }) => {
|
||||
setTableSize({ width, height });
|
||||
@@ -1155,42 +1100,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
|
||||
const { width: widthFromState, height: heightFromState } = tableSize;
|
||||
|
||||
const handleSortByChange = useCallback(
|
||||
(sortBy: SortByItem[]) => {
|
||||
if (!serverPagination) return;
|
||||
const modifiedOwnState = {
|
||||
...serverPaginationData,
|
||||
sortBy,
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
},
|
||||
[setDataMask, serverPagination],
|
||||
);
|
||||
|
||||
const handleSearch = (searchText: string) => {
|
||||
const modifiedOwnState = {
|
||||
...(serverPaginationData || {}),
|
||||
searchColumn:
|
||||
serverPaginationData?.searchColumn || searchOptions[0]?.value,
|
||||
searchText,
|
||||
currentPage: 0, // Reset to first page when searching
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
};
|
||||
|
||||
const debouncedSearch = debounce(handleSearch, 800);
|
||||
|
||||
const handleChangeSearchCol = (searchCol: string) => {
|
||||
if (!isEqual(searchCol, serverPaginationData?.searchColumn)) {
|
||||
const modifiedOwnState = {
|
||||
...(serverPaginationData || {}),
|
||||
searchColumn: searchCol,
|
||||
searchText: '',
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Styles>
|
||||
<DataTable<D>
|
||||
@@ -1206,9 +1115,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
serverPagination={serverPagination}
|
||||
onServerPaginationChange={handleServerPaginationChange}
|
||||
onColumnOrderChange={() => setColumnOrderToggle(!columnOrderToggle)}
|
||||
initialSearchText={serverPaginationData?.searchText || ''}
|
||||
sortByFromParent={serverPaginationData?.sortBy || []}
|
||||
searchInputId={`${slice_id}-search`}
|
||||
// 9 page items in > 340px works well even for 100+ pages
|
||||
maxPageItemCount={width > 340 ? 9 : 7}
|
||||
noResults={getNoResultsMessage}
|
||||
@@ -1222,11 +1128,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
renderTimeComparisonDropdown={
|
||||
isUsingTimeComparison ? renderTimeComparisonDropdown : undefined
|
||||
}
|
||||
handleSortByChange={handleSortByChange}
|
||||
onSearchColChange={handleChangeSearchCol}
|
||||
manualSearch={serverPagination}
|
||||
onSearchChange={debouncedSearch}
|
||||
searchOptions={searchOptions}
|
||||
/>
|
||||
</Styles>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
ensureIsArray,
|
||||
getMetricLabel,
|
||||
isPhysicalColumn,
|
||||
QueryFormOrderBy,
|
||||
QueryMode,
|
||||
QueryObject,
|
||||
removeDuplicates,
|
||||
@@ -35,7 +34,7 @@ import {
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { TableChartFormData } from './types';
|
||||
import { updateTableOwnState } from './DataTable/utils/externalAPIs';
|
||||
import { updateExternalFormData } from './DataTable/utils/externalAPIs';
|
||||
|
||||
/**
|
||||
* Infer query mode from form data. If `all_columns` is set, then raw records mode,
|
||||
@@ -192,40 +191,18 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
|
||||
const moreProps: Partial<QueryObject> = {};
|
||||
const ownState = options?.ownState ?? {};
|
||||
// Build Query flag to check if its for either download as csv, excel or json
|
||||
const isDownloadQuery =
|
||||
['csv', 'xlsx'].includes(formData?.result_format || '') ||
|
||||
(formData?.result_format === 'json' &&
|
||||
formData?.result_type === 'results');
|
||||
|
||||
if (isDownloadQuery) {
|
||||
moreProps.row_limit = Number(formDataCopy.row_limit) || 0;
|
||||
moreProps.row_offset = 0;
|
||||
}
|
||||
|
||||
if (!isDownloadQuery && formDataCopy.server_pagination) {
|
||||
const pageSize = ownState.pageSize ?? formDataCopy.server_page_length;
|
||||
const currentPage = ownState.currentPage ?? 0;
|
||||
|
||||
moreProps.row_limit = pageSize;
|
||||
moreProps.row_offset = currentPage * pageSize;
|
||||
}
|
||||
|
||||
// getting sort by in case of server pagination from own state
|
||||
let sortByFromOwnState: QueryFormOrderBy[] | undefined;
|
||||
if (Array.isArray(ownState?.sortBy) && ownState?.sortBy.length > 0) {
|
||||
const sortByItem = ownState?.sortBy[0];
|
||||
sortByFromOwnState = [[sortByItem?.key, !sortByItem?.desc]];
|
||||
if (formDataCopy.server_pagination) {
|
||||
moreProps.row_limit =
|
||||
ownState.pageSize ?? formDataCopy.server_page_length;
|
||||
moreProps.row_offset =
|
||||
(ownState.currentPage ?? 0) * (ownState.pageSize ?? 0);
|
||||
}
|
||||
|
||||
let queryObject = {
|
||||
...baseQueryObject,
|
||||
columns,
|
||||
extras,
|
||||
orderby:
|
||||
formData.server_pagination && sortByFromOwnState
|
||||
? sortByFromOwnState
|
||||
: orderby,
|
||||
orderby,
|
||||
metrics,
|
||||
post_processing: postProcessing,
|
||||
time_offsets: timeOffsets,
|
||||
@@ -239,12 +216,11 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
JSON.stringify(queryObject.filters)
|
||||
) {
|
||||
queryObject = { ...queryObject, row_offset: 0 };
|
||||
const modifiedOwnState = {
|
||||
...(options?.ownState || {}),
|
||||
currentPage: 0,
|
||||
pageSize: queryObject.row_limit ?? 0,
|
||||
};
|
||||
updateTableOwnState(options?.hooks?.setDataMask, modifiedOwnState);
|
||||
updateExternalFormData(
|
||||
options?.hooks?.setDataMask,
|
||||
0,
|
||||
queryObject.row_limit ?? 0,
|
||||
);
|
||||
}
|
||||
// Because we use same buildQuery for all table on the page we need split them by id
|
||||
options?.hooks?.setCachedChanges({
|
||||
@@ -276,32 +252,12 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
}
|
||||
|
||||
if (formData.server_pagination) {
|
||||
// Add search filter if search text exists
|
||||
if (ownState.searchText && ownState?.searchColumn) {
|
||||
queryObject = {
|
||||
...queryObject,
|
||||
filters: [
|
||||
...(queryObject.filters || []),
|
||||
{
|
||||
col: ownState?.searchColumn,
|
||||
op: 'ILIKE',
|
||||
val: `${ownState.searchText}%`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Now since row limit control is always visible even
|
||||
// in case of server pagination
|
||||
// we must use row limit from form data
|
||||
if (formData.server_pagination && !isDownloadQuery) {
|
||||
return [
|
||||
{ ...queryObject },
|
||||
{
|
||||
...queryObject,
|
||||
time_offsets: [],
|
||||
row_limit: Number(formData?.row_limit) ?? 0,
|
||||
row_limit: 0,
|
||||
row_offset: 0,
|
||||
post_processing: [],
|
||||
is_rowcount: true,
|
||||
|
||||
@@ -28,10 +28,7 @@ import {
|
||||
ControlStateMapping,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
Dataset,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
defineSavedMetrics,
|
||||
formatSelectOptions,
|
||||
getStandardizedControls,
|
||||
QueryModeLabel,
|
||||
sections,
|
||||
@@ -43,14 +40,11 @@ import {
|
||||
getMetricLabel,
|
||||
isAdhocColumn,
|
||||
isPhysicalColumn,
|
||||
legacyValidateInteger,
|
||||
QueryFormColumn,
|
||||
QueryFormMetric,
|
||||
QueryMode,
|
||||
SMART_DATE_ID,
|
||||
t,
|
||||
validateMaxValue,
|
||||
validateServerPagination,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import { isEmpty, last } from 'lodash';
|
||||
@@ -194,15 +188,6 @@ const processComparisonColumns = (columns: any[], suffix: string) =>
|
||||
})
|
||||
.flat();
|
||||
|
||||
/*
|
||||
Options for row limit control
|
||||
*/
|
||||
|
||||
export const ROW_LIMIT_OPTIONS_TABLE = [
|
||||
10, 50, 100, 250, 500, 1000, 5000, 10000, 50000, 100000, 150000, 200000,
|
||||
250000, 300000, 350000, 400000, 450000, 500000,
|
||||
];
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
@@ -357,6 +342,14 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'row_limit',
|
||||
override: {
|
||||
default: 1000,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
!controls?.server_pagination?.value,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'server_page_length',
|
||||
config: {
|
||||
@@ -371,47 +364,6 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'row_limit',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Row limit'),
|
||||
clearable: false,
|
||||
mapStateToProps: state => ({
|
||||
maxValue: state?.common?.conf?.TABLE_VIZ_MAX_ROW_SERVER,
|
||||
server_pagination: state?.form_data?.server_pagination,
|
||||
maxValueWithoutServerPagination:
|
||||
state?.common?.conf?.SQL_MAX_ROW,
|
||||
}),
|
||||
validators: [
|
||||
legacyValidateInteger,
|
||||
(v, state) =>
|
||||
validateMaxValue(
|
||||
v,
|
||||
state?.maxValue || DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
(v, state) =>
|
||||
validateServerPagination(
|
||||
v,
|
||||
state?.server_pagination,
|
||||
state?.maxValueWithoutServerPagination || DEFAULT_MAX_ROW,
|
||||
),
|
||||
],
|
||||
// Re run the validations when this control value
|
||||
validationDependancies: ['server_pagination'],
|
||||
default: 10000,
|
||||
choices: formatSelectOptions(ROW_LIMIT_OPTIONS_TABLE),
|
||||
description: t(
|
||||
'Limits the number of the rows that are computed in the query that is the source of the data used for this chart.',
|
||||
),
|
||||
},
|
||||
override: {
|
||||
default: 1000,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'order_desc',
|
||||
|
||||
@@ -90,15 +90,6 @@ const processDataRecords = memoizeOne(function processDataRecords(
|
||||
return data;
|
||||
});
|
||||
|
||||
// Create a map to store cached values per slice
|
||||
const sliceCache = new Map<
|
||||
number,
|
||||
{
|
||||
cachedServerLength: number;
|
||||
passedColumns?: DataColumnMeta[];
|
||||
}
|
||||
>();
|
||||
|
||||
const calculateDifferences = (
|
||||
originalValue: number,
|
||||
comparisonValue: number,
|
||||
@@ -489,7 +480,6 @@ const transformProps = (
|
||||
comparison_color_enabled: comparisonColorEnabled = false,
|
||||
comparison_color_scheme: comparisonColorScheme = ColorSchemeEnum.Green,
|
||||
comparison_type,
|
||||
slice_id,
|
||||
} = formData;
|
||||
const isUsingTimeComparison =
|
||||
!isEmpty(time_compare) &&
|
||||
@@ -685,26 +675,6 @@ const transformProps = (
|
||||
conditionalFormatting,
|
||||
);
|
||||
|
||||
// Get cached values for this slice
|
||||
const cachedValues = sliceCache.get(slice_id);
|
||||
let hasServerPageLengthChanged = false;
|
||||
|
||||
if (
|
||||
cachedValues?.cachedServerLength !== undefined &&
|
||||
cachedValues.cachedServerLength !== serverPageLength
|
||||
) {
|
||||
hasServerPageLengthChanged = true;
|
||||
}
|
||||
|
||||
// Update cache with new values
|
||||
sliceCache.set(slice_id, {
|
||||
cachedServerLength: serverPageLength,
|
||||
passedColumns:
|
||||
Array.isArray(passedColumns) && passedColumns?.length > 0
|
||||
? passedColumns
|
||||
: cachedValues?.passedColumns,
|
||||
});
|
||||
|
||||
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
||||
return {
|
||||
height,
|
||||
@@ -712,10 +682,7 @@ const transformProps = (
|
||||
isRawRecords: queryMode === QueryMode.Raw,
|
||||
data: passedData,
|
||||
totals,
|
||||
columns:
|
||||
Array.isArray(passedColumns) && passedColumns?.length > 0
|
||||
? passedColumns
|
||||
: cachedValues?.passedColumns || [],
|
||||
columns: passedColumns,
|
||||
serverPagination,
|
||||
metrics,
|
||||
percentMetrics,
|
||||
@@ -730,9 +697,7 @@ const transformProps = (
|
||||
includeSearch,
|
||||
rowCount,
|
||||
pageSize: serverPagination
|
||||
? serverPaginationData?.pageSize
|
||||
? serverPaginationData?.pageSize
|
||||
: serverPageLength
|
||||
? serverPageLength
|
||||
: getPageSize(pageLength, data.length, columns.length),
|
||||
filters: filterState.filters,
|
||||
emitCrossFilters,
|
||||
@@ -746,9 +711,6 @@ const transformProps = (
|
||||
basicColorFormatters,
|
||||
startDateOffset,
|
||||
basicColorColumnFormatters,
|
||||
hasServerPageLengthChanged,
|
||||
serverPageLength,
|
||||
slice_id,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -114,32 +114,13 @@ export type BasicColorFormatterType = {
|
||||
mainArrow: string;
|
||||
};
|
||||
|
||||
export type SortByItem = {
|
||||
id: string;
|
||||
key: string;
|
||||
desc?: boolean;
|
||||
};
|
||||
|
||||
export type SearchOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export interface ServerPaginationData {
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
sortBy?: SortByItem[];
|
||||
searchText?: string;
|
||||
searchColumn?: string;
|
||||
}
|
||||
|
||||
export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
|
||||
timeGrain?: TimeGranularity;
|
||||
height: number;
|
||||
width: number;
|
||||
rowCount?: number;
|
||||
serverPagination: boolean;
|
||||
serverPaginationData: ServerPaginationData;
|
||||
serverPaginationData: { pageSize?: number; currentPage?: number };
|
||||
setDataMask: SetDataMaskHook;
|
||||
isRawRecords?: boolean;
|
||||
data: D[];
|
||||
@@ -171,11 +152,6 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
|
||||
basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
||||
basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
||||
startDateOffset?: string;
|
||||
// For explore page to reset the server Pagination data
|
||||
// if server page length is changed from control panel
|
||||
hasServerPageLengthChanged: boolean;
|
||||
serverPageLength: number;
|
||||
slice_id: number;
|
||||
}
|
||||
|
||||
export enum ColorSchemeEnum {
|
||||
|
||||
@@ -327,10 +327,6 @@ class ChartRenderer extends Component {
|
||||
?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
|
||||
? { inContextMenu: this.state.inContextMenu }
|
||||
: {};
|
||||
// By pass no result component when server pagination is enabled & the table has a backend search query
|
||||
const bypassNoResult = !(
|
||||
formData?.server_pagination && (ownState?.searchText?.length || 0) > 0
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -371,7 +367,6 @@ class ChartRenderer extends Component {
|
||||
postTransformProps={postTransformProps}
|
||||
emitCrossFilters={emitCrossFilters}
|
||||
legendState={this.state.legendState}
|
||||
enableNoResults={bypassNoResult}
|
||||
{...drillToDetailProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
t,
|
||||
withTheme,
|
||||
getClientErrorObject,
|
||||
getExtensionsRegistry,
|
||||
} from '@superset-ui/core';
|
||||
import { Select, AsyncSelect, Row, Col } from 'src/components';
|
||||
import { FormLabel } from 'src/components/Form';
|
||||
@@ -54,15 +53,10 @@ import SpatialControl from 'src/explore/components/controls/SpatialControl';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import { Icons } from 'src/components/Icons';
|
||||
import CurrencyControl from 'src/explore/components/controls/CurrencyControl';
|
||||
import { executeQuery, resetDatabaseState } from 'src/database/actions';
|
||||
import { connect } from 'react-redux';
|
||||
import CollectionTable from './CollectionTable';
|
||||
import Fieldset from './Fieldset';
|
||||
import Field from './Field';
|
||||
import { fetchSyncedColumns, updateColumns } from './utils';
|
||||
import FilterableTable from '../FilterableTable';
|
||||
|
||||
const extensionsRegistry = getExtensionsRegistry();
|
||||
|
||||
const DatasourceContainer = styled.div`
|
||||
.change-warning {
|
||||
@@ -592,8 +586,6 @@ function OwnersSelector({ datasource, onChange }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
const ResultTable =
|
||||
extensionsRegistry.get('sqleditor.extension.resultTable') ?? FilterableTable;
|
||||
|
||||
class DatasourceEditor extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -706,23 +698,6 @@ class DatasourceEditor extends PureComponent {
|
||||
this.validate(this.onChange);
|
||||
}
|
||||
|
||||
async onQueryRun() {
|
||||
this.props.runQuery({
|
||||
client_id: this.props.clientId,
|
||||
database_id: this.state.datasource.database.id,
|
||||
json: true,
|
||||
runAsync: false,
|
||||
catalog: this.state.datasource.catalog,
|
||||
schema: this.state.datasource.schema,
|
||||
sql: this.state.datasource.sql,
|
||||
tmp_table_name: '',
|
||||
select_as_cta: false,
|
||||
ctas_method: 'TABLE',
|
||||
queryLimit: 25,
|
||||
expand_data: true,
|
||||
});
|
||||
}
|
||||
|
||||
tableChangeAndSyncMetadata() {
|
||||
this.validate(() => {
|
||||
this.syncMetadata();
|
||||
@@ -1103,62 +1078,14 @@ class DatasourceEditor extends PureComponent {
|
||||
<TextAreaControl
|
||||
language="sql"
|
||||
offerEditInModal={false}
|
||||
minLines={10}
|
||||
minLines={20}
|
||||
maxLines={Infinity}
|
||||
readOnly={!this.state.isEditMode}
|
||||
resize="both"
|
||||
tooltipOptions={sqlTooltipOptions}
|
||||
/>
|
||||
}
|
||||
additionalControl={
|
||||
<div
|
||||
css={css`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
css={css`
|
||||
align-self: flex-end;
|
||||
height: 24px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
`}
|
||||
size="small"
|
||||
buttonStyle="primary"
|
||||
onClick={() => {
|
||||
this.onQueryRun();
|
||||
}}
|
||||
>
|
||||
<Icons.CaretRightFilled
|
||||
iconSize="s"
|
||||
css={theme => ({
|
||||
color: theme.colors.grayscale.light5,
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
errorMessage={
|
||||
this.props.database?.error && t('Error executing query.')
|
||||
}
|
||||
/>
|
||||
{this.props.database?.queryResult && (
|
||||
<ResultTable
|
||||
data={this.props.database.queryResult.data}
|
||||
queryId={this.props.database.queryResult.query.id}
|
||||
orderedColumnKeys={this.props.database.queryResult.columns.map(
|
||||
col => col.column_name,
|
||||
)}
|
||||
height={100}
|
||||
expandedColumns={
|
||||
this.props.database.queryResult.expandedColumns
|
||||
}
|
||||
allowHTML
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1539,10 +1466,6 @@ class DatasourceEditor extends PureComponent {
|
||||
</DatasourceContainer>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.resetQuery();
|
||||
}
|
||||
}
|
||||
|
||||
DatasourceEditor.defaultProps = defaultProps;
|
||||
@@ -1550,14 +1473,4 @@ DatasourceEditor.propTypes = propTypes;
|
||||
|
||||
const DataSourceComponent = withTheme(DatasourceEditor);
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
runQuery: payload => dispatch(executeQuery(payload)),
|
||||
resetQuery: () => dispatch(resetDatabaseState()),
|
||||
});
|
||||
const mapStateToProps = state => ({
|
||||
test: state.queryApi,
|
||||
database: state.database,
|
||||
});
|
||||
export default withToasts(
|
||||
connect(mapStateToProps, mapDispatchToProps)(DataSourceComponent),
|
||||
);
|
||||
export default withToasts(DataSourceComponent);
|
||||
|
||||
@@ -120,83 +120,71 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const dialog = useRef<any>(null);
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const buildPayload = (datasource: Record<string, any>) => {
|
||||
const payload: Record<string, any> = {
|
||||
table_name: datasource.table_name,
|
||||
database_id: datasource.database?.id,
|
||||
sql: datasource.sql,
|
||||
filter_select_enabled: datasource.filter_select_enabled,
|
||||
fetch_values_predicate: datasource.fetch_values_predicate,
|
||||
schema:
|
||||
datasource.tableSelector?.schema ||
|
||||
datasource.databaseSelector?.schema ||
|
||||
datasource.schema,
|
||||
description: datasource.description,
|
||||
main_dttm_col: datasource.main_dttm_col,
|
||||
normalize_columns: datasource.normalize_columns,
|
||||
always_filter_main_dttm: datasource.always_filter_main_dttm,
|
||||
offset: datasource.offset,
|
||||
default_endpoint: datasource.default_endpoint,
|
||||
cache_timeout:
|
||||
datasource.cache_timeout === '' ? null : datasource.cache_timeout,
|
||||
is_sqllab_view: datasource.is_sqllab_view,
|
||||
template_params: datasource.template_params,
|
||||
extra: datasource.extra,
|
||||
is_managed_externally: datasource.is_managed_externally,
|
||||
external_url: datasource.external_url,
|
||||
metrics: datasource?.metrics?.map(
|
||||
(metric: DatasetObject['metrics'][0]) => {
|
||||
const metricBody: any = {
|
||||
expression: metric.expression,
|
||||
description: metric.description,
|
||||
metric_name: metric.metric_name,
|
||||
metric_type: metric.metric_type,
|
||||
d3format: metric.d3format || null,
|
||||
currency: !isDefined(metric.currency)
|
||||
? null
|
||||
: JSON.stringify(metric.currency),
|
||||
verbose_name: metric.verbose_name,
|
||||
warning_text: metric.warning_text,
|
||||
uuid: metric.uuid,
|
||||
extra: buildExtraJsonObject(metric),
|
||||
};
|
||||
if (!Number.isNaN(Number(metric.id))) {
|
||||
metricBody.id = metric.id;
|
||||
}
|
||||
return metricBody;
|
||||
},
|
||||
),
|
||||
columns: datasource?.columns?.map(
|
||||
(column: DatasetObject['columns'][0]) => ({
|
||||
id: typeof column.id === 'number' ? column.id : undefined,
|
||||
column_name: column.column_name,
|
||||
type: column.type,
|
||||
advanced_data_type: column.advanced_data_type,
|
||||
verbose_name: column.verbose_name,
|
||||
description: column.description,
|
||||
expression: column.expression,
|
||||
filterable: column.filterable,
|
||||
groupby: column.groupby,
|
||||
is_active: column.is_active,
|
||||
is_dttm: column.is_dttm,
|
||||
python_date_format: column.python_date_format || null,
|
||||
uuid: column.uuid,
|
||||
extra: buildExtraJsonObject(column),
|
||||
}),
|
||||
),
|
||||
owners: datasource.owners.map(
|
||||
(o: Record<string, number>) => o.value || o.id,
|
||||
),
|
||||
};
|
||||
// Handle catalog based on database's allow_multi_catalog setting
|
||||
// If multi-catalog is disabled, don't include catalog in payload
|
||||
// The backend will use the default catalog
|
||||
// If multi-catalog is enabled, include the selected catalog
|
||||
if (datasource.database?.allow_multi_catalog) {
|
||||
payload.catalog = datasource.catalog;
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
const buildPayload = (datasource: Record<string, any>) => ({
|
||||
table_name: datasource.table_name,
|
||||
database_id: datasource.database?.id,
|
||||
sql: datasource.sql,
|
||||
filter_select_enabled: datasource.filter_select_enabled,
|
||||
fetch_values_predicate: datasource.fetch_values_predicate,
|
||||
schema:
|
||||
datasource.tableSelector?.schema ||
|
||||
datasource.databaseSelector?.schema ||
|
||||
datasource.schema,
|
||||
description: datasource.description,
|
||||
main_dttm_col: datasource.main_dttm_col,
|
||||
normalize_columns: datasource.normalize_columns,
|
||||
always_filter_main_dttm: datasource.always_filter_main_dttm,
|
||||
offset: datasource.offset,
|
||||
default_endpoint: datasource.default_endpoint,
|
||||
cache_timeout:
|
||||
datasource.cache_timeout === '' ? null : datasource.cache_timeout,
|
||||
is_sqllab_view: datasource.is_sqllab_view,
|
||||
template_params: datasource.template_params,
|
||||
extra: datasource.extra,
|
||||
is_managed_externally: datasource.is_managed_externally,
|
||||
external_url: datasource.external_url,
|
||||
metrics: datasource?.metrics?.map((metric: DatasetObject['metrics'][0]) => {
|
||||
const metricBody: any = {
|
||||
expression: metric.expression,
|
||||
description: metric.description,
|
||||
metric_name: metric.metric_name,
|
||||
metric_type: metric.metric_type,
|
||||
d3format: metric.d3format || null,
|
||||
currency: !isDefined(metric.currency)
|
||||
? null
|
||||
: JSON.stringify(metric.currency),
|
||||
verbose_name: metric.verbose_name,
|
||||
warning_text: metric.warning_text,
|
||||
uuid: metric.uuid,
|
||||
extra: buildExtraJsonObject(metric),
|
||||
};
|
||||
if (!Number.isNaN(Number(metric.id))) {
|
||||
metricBody.id = metric.id;
|
||||
}
|
||||
return metricBody;
|
||||
}),
|
||||
columns: datasource?.columns?.map(
|
||||
(column: DatasetObject['columns'][0]) => ({
|
||||
id: typeof column.id === 'number' ? column.id : undefined,
|
||||
column_name: column.column_name,
|
||||
type: column.type,
|
||||
advanced_data_type: column.advanced_data_type,
|
||||
verbose_name: column.verbose_name,
|
||||
description: column.description,
|
||||
expression: column.expression,
|
||||
filterable: column.filterable,
|
||||
groupby: column.groupby,
|
||||
is_active: column.is_active,
|
||||
is_dttm: column.is_dttm,
|
||||
python_date_format: column.python_date_format || null,
|
||||
uuid: column.uuid,
|
||||
extra: buildExtraJsonObject(column),
|
||||
}),
|
||||
),
|
||||
owners: datasource.owners.map(
|
||||
(o: Record<string, number>) => o.value || o.id,
|
||||
),
|
||||
});
|
||||
const onConfirmSave = async () => {
|
||||
// Pull out extra fields into the extra object
|
||||
setIsSaving(true);
|
||||
|
||||
@@ -29,20 +29,13 @@ const defaultProps = {
|
||||
onChange: jest.fn(),
|
||||
compact: false,
|
||||
inline: false,
|
||||
additionalControl: (
|
||||
<input type="button" data-test="mock-text-aditional-control" />
|
||||
),
|
||||
};
|
||||
|
||||
test('should render', () => {
|
||||
const { container } = render(<Field {...defaultProps} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
test('should render with aditional control', () => {
|
||||
const { getByTestId } = render(<Field {...defaultProps} />);
|
||||
const additionalControl = getByTestId('mock-text-aditional-control');
|
||||
expect(additionalControl).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call onChange', () => {
|
||||
const { getByTestId } = render(<Field {...defaultProps} />);
|
||||
const textArea = getByTestId('mock-text-control');
|
||||
@@ -54,9 +47,3 @@ test('should render compact', () => {
|
||||
render(<Field {...defaultProps} compact />);
|
||||
expect(screen.queryByText(defaultProps.description)).not.toBeInTheDocument();
|
||||
});
|
||||
test('shiuld render error message', () => {
|
||||
const { getByText } = render(
|
||||
<Field {...defaultProps} errorMessage="error message" />,
|
||||
);
|
||||
expect(getByText('error message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useCallback, ReactNode, ReactElement, cloneElement } from 'react';
|
||||
import { css, SupersetTheme } from '@superset-ui/core';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { FormItem, FormLabel } from 'src/components/Form';
|
||||
import { Icons } from 'src/components/Icons';
|
||||
|
||||
const formItemInlineCss = css`
|
||||
.ant-form-item-control-input-content {
|
||||
@@ -29,17 +28,16 @@ const formItemInlineCss = css`
|
||||
flex-direction: row;
|
||||
}
|
||||
`;
|
||||
|
||||
interface FieldProps<V> {
|
||||
fieldKey: string;
|
||||
value?: V;
|
||||
label: string;
|
||||
description?: ReactNode;
|
||||
control: ReactElement;
|
||||
additionalControl?: ReactElement;
|
||||
onChange: (fieldKey: string, newValue: V) => void;
|
||||
compact: boolean;
|
||||
inline: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export default function Field<V>({
|
||||
@@ -48,11 +46,9 @@ export default function Field<V>({
|
||||
label,
|
||||
description = null,
|
||||
control,
|
||||
additionalControl,
|
||||
onChange = () => {},
|
||||
compact = false,
|
||||
inline,
|
||||
errorMessage,
|
||||
}: FieldProps<V>) {
|
||||
const onControlChange = useCallback(
|
||||
newValue => {
|
||||
@@ -66,51 +62,32 @@ export default function Field<V>({
|
||||
onChange: onControlChange,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
css={
|
||||
additionalControl &&
|
||||
css`
|
||||
position: relative;
|
||||
`
|
||||
<FormItem
|
||||
label={
|
||||
<FormLabel className="m-r-5">
|
||||
{label || fieldKey}
|
||||
{compact && description && (
|
||||
<Tooltip id="field-descr" placement="right" title={description}>
|
||||
{/* TODO: Remove fa-icon */}
|
||||
{/* eslint-disable-next-line icons/no-fa-icons-usage */}
|
||||
<i className="fa fa-info-circle m-l-5" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</FormLabel>
|
||||
}
|
||||
css={inline && formItemInlineCss}
|
||||
>
|
||||
{additionalControl}
|
||||
<FormItem
|
||||
label={
|
||||
<FormLabel className="m-r-5">
|
||||
{label || fieldKey}
|
||||
{compact && description && (
|
||||
<Tooltip id="field-descr" placement="right" title={description}>
|
||||
<Icons.InfoCircleFilled iconSize="s" className="m-l-5" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</FormLabel>
|
||||
}
|
||||
css={inline && formItemInlineCss}
|
||||
>
|
||||
{hookedControl}
|
||||
{!compact && description && (
|
||||
<div
|
||||
css={(theme: SupersetTheme) => ({
|
||||
color: theme.colors.grayscale.base,
|
||||
[inline ? 'marginLeft' : 'marginTop']: theme.gridUnit,
|
||||
})}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
{errorMessage && (
|
||||
{hookedControl}
|
||||
{!compact && description && (
|
||||
<div
|
||||
css={(theme: SupersetTheme) => ({
|
||||
color: theme.colors.error.base,
|
||||
marginTop: -16,
|
||||
fontSize: theme.typography.sizes.s,
|
||||
color: theme.colors.grayscale.base,
|
||||
[inline ? 'marginLeft' : 'marginTop']: theme.gridUnit,
|
||||
})}
|
||||
>
|
||||
{errorMessage}
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
CaretDownOutlined,
|
||||
CaretLeftOutlined,
|
||||
CaretRightOutlined,
|
||||
CaretRightFilled,
|
||||
CalendarOutlined,
|
||||
CheckOutlined,
|
||||
CheckCircleOutlined,
|
||||
@@ -135,7 +134,6 @@ const AntdIcons = {
|
||||
CaretDownOutlined,
|
||||
CaretLeftOutlined,
|
||||
CaretRightOutlined,
|
||||
CaretRightFilled,
|
||||
CalendarOutlined,
|
||||
CheckOutlined,
|
||||
CheckCircleOutlined,
|
||||
|
||||
@@ -440,7 +440,7 @@ const Select = forwardRef(
|
||||
|
||||
const bulkSelectComponent = useMemo(
|
||||
() => (
|
||||
<StyledBulkActionsContainer className="select-bulk-actions" size={0}>
|
||||
<StyledBulkActionsContainer size={0}>
|
||||
<Button
|
||||
type="link"
|
||||
buttonSize="xsmall"
|
||||
|
||||
@@ -51,7 +51,6 @@ import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
|
||||
import { componentShape } from 'src/dashboard/util/propShapes';
|
||||
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
|
||||
import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
|
||||
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
|
||||
import { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants';
|
||||
import { isCurrentUserBot } from 'src/utils/isBot';
|
||||
import { useDebouncedEffect } from '../../../explore/exploreUtils';
|
||||
@@ -189,10 +188,7 @@ const Row = props => {
|
||||
observerDisabler = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (!entry.isIntersecting && isComponentVisibleRef.current) {
|
||||
// Reference: https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-rootmargin
|
||||
if (!isEmbedded()) {
|
||||
setIsInView(false);
|
||||
}
|
||||
setIsInView(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
-12
@@ -128,14 +128,6 @@ const VerticalFormItem = styled(StyledFormItem)<{
|
||||
width: 140px;
|
||||
`}
|
||||
}
|
||||
|
||||
.select-bulk-actions {
|
||||
${({ inverseSelection }) =>
|
||||
inverseSelection &&
|
||||
`
|
||||
flex-direction: column;
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
const HorizontalFormItem = styled(StyledFormItem)<{
|
||||
@@ -172,10 +164,6 @@ const HorizontalFormItem = styled(StyledFormItem)<{
|
||||
width: 164px;
|
||||
`}
|
||||
}
|
||||
|
||||
.select-bulk-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const HorizontalOverflowFormItem = VerticalFormItem;
|
||||
|
||||
-2
@@ -223,7 +223,6 @@ describe('ColumnSelect filterValues behavior', () => {
|
||||
...createProps(),
|
||||
formFilter: { filterType: 'filterType' },
|
||||
};
|
||||
// @ts-ignore: bypass incomplete formFilter type for test
|
||||
const element = getControlItemsMap(props).mainControlItems.groupby
|
||||
.element as React.ReactElement;
|
||||
render(element);
|
||||
@@ -238,7 +237,6 @@ describe('ColumnSelect filterValues behavior', () => {
|
||||
...createProps(),
|
||||
formFilter: { filterType: 'filterType' },
|
||||
};
|
||||
// @ts-ignore: bypass incomplete formFilter type for test
|
||||
const element = getControlItemsMap(props).mainControlItems.groupby
|
||||
.element as React.ReactElement;
|
||||
render(element);
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ export default function getControlItemsMap({
|
||||
doesColumnMatchFilterType(
|
||||
formFilter?.filterType || '',
|
||||
column,
|
||||
) && !!column?.filterable
|
||||
) && column.filterable
|
||||
}
|
||||
onChange={() => {
|
||||
// We need reset default value when column changed
|
||||
|
||||
@@ -29,6 +29,7 @@ export const PLACEHOLDER_DATASOURCE: Datasource = {
|
||||
column_types: [],
|
||||
metrics: [],
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '',
|
||||
description: '',
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const isEmbedded = () => {
|
||||
try {
|
||||
return window.self !== window.top || window.frameElement !== null;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -27,13 +27,10 @@ import {
|
||||
Filter,
|
||||
FilterConfiguration,
|
||||
Filters,
|
||||
FilterState,
|
||||
ExtraFormData,
|
||||
} from '@superset-ui/core';
|
||||
import { NATIVE_FILTER_PREFIX } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/utils';
|
||||
import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
|
||||
import { SaveFilterChangesType } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/types';
|
||||
import { isEqual } from 'lodash';
|
||||
import {
|
||||
AnyDataMaskAction,
|
||||
CLEAR_DATA_MASK_STATE,
|
||||
@@ -42,11 +39,6 @@ import {
|
||||
} from './actions';
|
||||
import { areObjectsEqual } from '../reduxUtils';
|
||||
|
||||
type FilterWithExtaFromData = Filter & {
|
||||
extraFormData?: ExtraFormData;
|
||||
filterState?: FilterState;
|
||||
};
|
||||
|
||||
export function getInitialDataMask(
|
||||
id?: string | number,
|
||||
moreProps: DataMask = {},
|
||||
@@ -114,27 +106,10 @@ function updateDataMaskForFilterChanges(
|
||||
});
|
||||
|
||||
filterChanges.modified.forEach((filter: Filter) => {
|
||||
const existingFilter = draftDataMask[filter.id] as FilterWithExtaFromData;
|
||||
|
||||
// Check if targets are equal
|
||||
const areTargetsEqual = isEqual(existingFilter?.targets, filter?.targets);
|
||||
|
||||
// Preserve state only if filter exists, has enableEmptyFilter=true and targets match
|
||||
const shouldPreserveState =
|
||||
existingFilter &&
|
||||
areTargetsEqual &&
|
||||
(filter.controlValues?.enableEmptyFilter ||
|
||||
filter.controlValues?.defaultToFirstItem);
|
||||
|
||||
mergedDataMask[filter.id] = {
|
||||
...getInitialDataMask(filter.id),
|
||||
...filter.defaultDataMask,
|
||||
...filter,
|
||||
// Preserve extraFormData and filterState if conditions match
|
||||
...(shouldPreserveState && {
|
||||
extraFormData: existingFilter.extraFormData,
|
||||
filterState: existingFilter.filterState,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { makeApi } from '@superset-ui/core';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { AnyAction } from 'redux';
|
||||
import { QueryExecutePayload, QueryExecuteResponse } from './types';
|
||||
|
||||
export const executeQueryApi = makeApi<
|
||||
QueryExecutePayload,
|
||||
QueryExecuteResponse
|
||||
>({
|
||||
method: 'POST',
|
||||
endpoint: '/api/v1/sqllab/execute',
|
||||
});
|
||||
|
||||
export function setQueryIsLoading(isLoading: boolean) {
|
||||
return {
|
||||
type: 'SET_QUERY_IS_LOADING',
|
||||
payload: isLoading,
|
||||
};
|
||||
}
|
||||
export function setQueryResult(queryResult: QueryExecuteResponse) {
|
||||
return {
|
||||
type: 'SET_QUERY_RESULT',
|
||||
payload: queryResult,
|
||||
};
|
||||
}
|
||||
export function resetDatabaseState() {
|
||||
return {
|
||||
type: 'RESET_DATABASE_STATE',
|
||||
};
|
||||
}
|
||||
export function setQueryError(error: string) {
|
||||
return {
|
||||
type: 'SET_QUERY_ERROR',
|
||||
payload: error,
|
||||
};
|
||||
}
|
||||
export function executeQuery(payload: QueryExecutePayload) {
|
||||
return async function (dispatch: ThunkDispatch<any, undefined, AnyAction>) {
|
||||
try {
|
||||
dispatch(setQueryIsLoading(true));
|
||||
const result = await executeQueryApi(payload);
|
||||
dispatch(setQueryResult(result as QueryExecuteResponse));
|
||||
} catch (error) {
|
||||
dispatch(setQueryError(error.message));
|
||||
} finally {
|
||||
dispatch(setQueryIsLoading(false));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { QueryAdhocState } from './types';
|
||||
|
||||
const initialState: QueryAdhocState = {
|
||||
isLoading: null,
|
||||
sql: null,
|
||||
queryResult: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export default function databaseReducer(
|
||||
state: QueryAdhocState = initialState,
|
||||
action: any,
|
||||
): QueryAdhocState {
|
||||
switch (action.type) {
|
||||
case 'SET_QUERY_IS_LOADING':
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload,
|
||||
};
|
||||
case 'SET_QUERY_RESULT':
|
||||
return {
|
||||
...state,
|
||||
sql: action.payload.query.sql ?? '',
|
||||
queryResult: action.payload,
|
||||
error: null,
|
||||
};
|
||||
case 'SET_QUERY_ERROR':
|
||||
return {
|
||||
...initialState,
|
||||
error: action.payload,
|
||||
};
|
||||
case 'RESET_DATABASE_STATE':
|
||||
return initialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export interface QueryExecutePayload {
|
||||
client_id: string;
|
||||
database_id: number;
|
||||
json: boolean;
|
||||
runAsync: boolean;
|
||||
catalog: string | null;
|
||||
schema: string;
|
||||
sql: string;
|
||||
tmp_table_name: string;
|
||||
select_as_cta: boolean;
|
||||
ctas_method: string;
|
||||
queryLimit: number;
|
||||
expand_data: boolean;
|
||||
}
|
||||
export interface Column {
|
||||
name: string;
|
||||
type: string;
|
||||
is_dttm: boolean;
|
||||
type_generic: number;
|
||||
is_hidden: boolean;
|
||||
column_name: string;
|
||||
}
|
||||
export interface QueryExecuteResponse {
|
||||
status: string;
|
||||
query_id: string;
|
||||
data: any[];
|
||||
columns: Column[];
|
||||
selected_columns: Column[];
|
||||
expanded_columns: Column[];
|
||||
query: any;
|
||||
}
|
||||
|
||||
export interface QueryAdhocState {
|
||||
isLoading: boolean | null;
|
||||
sql: string | null;
|
||||
queryResult: QueryExecuteResponse | null;
|
||||
error: string | null;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||
import { makeApi, t, logging } from '@superset-ui/core';
|
||||
import Switchboard from '@superset-ui/switchboard';
|
||||
import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
|
||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||
import setupClient from 'src/setup/setupClient';
|
||||
import setupPlugins from 'src/setup/setupPlugins';
|
||||
import { useUiConfig } from 'src/components/UiConfigContext';
|
||||
@@ -94,7 +94,7 @@ const EmbeddedRoute = () => (
|
||||
);
|
||||
|
||||
const EmbeddedApp = () => (
|
||||
<Router basename={applicationRoot()}>
|
||||
<Router>
|
||||
{/* todo (embedded) remove this line after uuids are deployed */}
|
||||
<Route path="/dashboard/:idOrSlug/embedded/" component={EmbeddedRoute} />
|
||||
<Route path="/embedded/:uuid/" component={EmbeddedRoute} />
|
||||
@@ -187,7 +187,6 @@ function start() {
|
||||
*/
|
||||
function setupGuestClient(guestToken: string) {
|
||||
setupClient({
|
||||
appRoot: applicationRoot(),
|
||||
guestToken,
|
||||
guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
|
||||
unauthorizedHandler: guestUnauthorizedHandler,
|
||||
|
||||
@@ -41,6 +41,7 @@ const CURRENT_DATASOURCE = {
|
||||
columns: [],
|
||||
metrics: [],
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '__timestamp',
|
||||
// eg. ['["ds", true]', 'ds [asc]']
|
||||
@@ -54,6 +55,7 @@ const NEW_DATASOURCE = {
|
||||
columns: [],
|
||||
metrics: [],
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '__timestamp',
|
||||
// eg. ['["ds", true]', 'ds [asc]']
|
||||
|
||||
@@ -56,6 +56,7 @@ describe('controlUtils', () => {
|
||||
{ metric_name: 'second', uuid: '2' },
|
||||
],
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '',
|
||||
datasource_name: '1__table',
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ const sampleDatasource: Dataset = {
|
||||
],
|
||||
metrics: [{ metric_name: 'saved_metric_2', uuid: '1' }],
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '',
|
||||
datasource_name: 'Sample Dataset',
|
||||
|
||||
@@ -138,6 +138,7 @@ export const exploreInitialData: ExplorePageInitialData = {
|
||||
{ metric_name: 'second', uuid: '2' },
|
||||
],
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '',
|
||||
datasource_name: '8__table',
|
||||
@@ -156,6 +157,7 @@ export const fallbackExploreInitialData: ExplorePageInitialData = {
|
||||
columns: [],
|
||||
metrics: [],
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '',
|
||||
owners: [],
|
||||
|
||||
@@ -214,52 +214,6 @@ export default function exploreReducer(state = {}, action) {
|
||||
currentControlsState = transformed.controlsState;
|
||||
}
|
||||
|
||||
const dependantControls = Object.entries(state.controls)
|
||||
.filter(
|
||||
([, item]) =>
|
||||
Array.isArray(item?.validationDependancies) &&
|
||||
item.validationDependancies.includes(controlName),
|
||||
)
|
||||
.map(([key, item]) => ({
|
||||
controlState: item,
|
||||
dependantControlName: key,
|
||||
}));
|
||||
|
||||
let updatedControlStates = {};
|
||||
if (dependantControls.length > 0) {
|
||||
const updatedControls = dependantControls.map(
|
||||
({ controlState, dependantControlName }) => {
|
||||
// overwrite state form data with current control value as the redux state will not
|
||||
// have latest action value
|
||||
const overWrittenState = {
|
||||
...state,
|
||||
form_data: {
|
||||
...state.form_data,
|
||||
[controlName]: action.value,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
// Re run validation for dependant controls
|
||||
controlState: getControlStateFromControlConfig(
|
||||
controlState,
|
||||
overWrittenState,
|
||||
controlState?.value,
|
||||
),
|
||||
dependantControlName,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
updatedControlStates = updatedControls.reduce(
|
||||
(acc, { controlState, dependantControlName }) => {
|
||||
acc[dependantControlName] = { ...controlState };
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
form_data: new_form_data,
|
||||
@@ -273,7 +227,6 @@ export default function exploreReducer(state = {}, action) {
|
||||
},
|
||||
}),
|
||||
...rerenderedControls,
|
||||
...updatedControlStates,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -62,7 +62,6 @@ export type DatasetObject = {
|
||||
filter_select_enabled?: boolean;
|
||||
fetch_values_predicate?: string;
|
||||
schema?: string;
|
||||
catalog?: string;
|
||||
description: string | null;
|
||||
main_dttm_col: string;
|
||||
offset?: number;
|
||||
@@ -79,6 +78,7 @@ export type DatasetObject = {
|
||||
always_filter_main_dttm: boolean;
|
||||
type: DatasourceType;
|
||||
column_formats: Record<string, string>;
|
||||
currency_formats: Record<string, Currency>;
|
||||
datasource_name: string | null;
|
||||
verbose_map: Record<string, string>;
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
AppSection,
|
||||
DataMask,
|
||||
@@ -84,10 +84,7 @@ function reducer(draft: DataMask, action: DataMaskAction) {
|
||||
}
|
||||
}
|
||||
|
||||
const StyledSpace = styled(Space)<{
|
||||
inverseSelection: boolean;
|
||||
appSection: AppSection;
|
||||
}>`
|
||||
const StyledSpace = styled(Space)<{ $inverseSelection: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
@@ -99,17 +96,12 @@ const StyledSpace = styled(Space)<{
|
||||
|
||||
&.ant-space {
|
||||
.ant-space-item {
|
||||
width: ${({ inverseSelection, appSection }) =>
|
||||
!inverseSelection || appSection === AppSection.FilterConfigModal
|
||||
? '100%'
|
||||
: 'auto'};
|
||||
width: ${({ $inverseSelection }) =>
|
||||
!$inverseSelection ? '100%' : 'auto'};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Keep track of orientation changes outside component with filter ID
|
||||
const orientationMap = new Map<string, FilterBarOrientation>();
|
||||
|
||||
export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
const {
|
||||
coltypeMap,
|
||||
@@ -166,30 +158,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
: filterState?.excludeFilterValues,
|
||||
);
|
||||
|
||||
const prevExcludeFilterValues = useRef(excludeFilterValues);
|
||||
|
||||
const hasOnlyOrientationChanged = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Get previous orientation for this specific filter
|
||||
const previousOrientation = orientationMap.get(formData.nativeFilterId);
|
||||
|
||||
// Check if only orientation changed for this filter
|
||||
if (
|
||||
previousOrientation !== undefined &&
|
||||
previousOrientation !== filterBarOrientation
|
||||
) {
|
||||
hasOnlyOrientationChanged.current = true;
|
||||
} else {
|
||||
hasOnlyOrientationChanged.current = false;
|
||||
}
|
||||
|
||||
// Update orientation for this filter
|
||||
if (filterBarOrientation) {
|
||||
orientationMap.set(formData.nativeFilterId, filterBarOrientation);
|
||||
}
|
||||
}, [filterBarOrientation]);
|
||||
|
||||
const updateDataMask = useCallback(
|
||||
(values: SelectValue) => {
|
||||
const emptyFilter =
|
||||
@@ -319,44 +287,35 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
[formData.sortAscending],
|
||||
);
|
||||
|
||||
// Use effect for initialisation for filter plugin
|
||||
// this should run only once when filter is configured & saved
|
||||
// & shouldnt run when the component is remounted on change of
|
||||
// orientation of filter bar
|
||||
useEffect(() => {
|
||||
// Skip if only orientation changed
|
||||
if (hasOnlyOrientationChanged.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 1: Handle disabled state first
|
||||
if (isDisabled) {
|
||||
updateDataMask(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: Handle the default to first Value case
|
||||
if (defaultToFirstItem) {
|
||||
// Set to first item if defaultToFirstItem is true
|
||||
if (defaultToFirstItem && filterState.value === undefined) {
|
||||
// initialize to first value if set to default to first item
|
||||
const firstItem: SelectValue = data[0]
|
||||
? (groupby.map(col => data[0][col]) as string[])
|
||||
: null;
|
||||
// firstItem[0] !== undefined for a case when groupby changed but new data still not fetched
|
||||
// TODO: still need repopulate default value in config modal when column changed
|
||||
if (firstItem?.[0] !== undefined) {
|
||||
updateDataMask(firstItem);
|
||||
}
|
||||
} else if (formData?.defaultValue) {
|
||||
// Case 3 : Handle defalut value case
|
||||
updateDataMask(formData.defaultValue);
|
||||
} else if (isDisabled) {
|
||||
// empty selection if filter is disabled
|
||||
updateDataMask(null);
|
||||
} else {
|
||||
// reset data mask based on filter state
|
||||
updateDataMask(filterState.value);
|
||||
}
|
||||
}, [
|
||||
col,
|
||||
isDisabled,
|
||||
enableEmptyFilter,
|
||||
defaultToFirstItem,
|
||||
formData?.defaultValue,
|
||||
enableEmptyFilter,
|
||||
inverseSelection,
|
||||
excludeFilterValues,
|
||||
updateDataMask,
|
||||
data,
|
||||
groupby,
|
||||
col,
|
||||
inverseSelection,
|
||||
JSON.stringify(filterState.value),
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -364,26 +323,23 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
}, [JSON.stringify(dataMask)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevExcludeFilterValues.current !== excludeFilterValues) {
|
||||
dispatchDataMask({
|
||||
type: 'filterState',
|
||||
extraFormData: getSelectExtraFormData(
|
||||
col,
|
||||
filterState.value,
|
||||
!filterState.value?.length,
|
||||
excludeFilterValues && inverseSelection,
|
||||
),
|
||||
filterState: {
|
||||
...(filterState as {
|
||||
value: SelectValue;
|
||||
label?: string;
|
||||
excludeFilterValues?: boolean;
|
||||
}),
|
||||
excludeFilterValues,
|
||||
},
|
||||
});
|
||||
prevExcludeFilterValues.current = excludeFilterValues;
|
||||
}
|
||||
dispatchDataMask({
|
||||
type: 'filterState',
|
||||
extraFormData: getSelectExtraFormData(
|
||||
col,
|
||||
filterState.value,
|
||||
!filterState.value?.length,
|
||||
excludeFilterValues && inverseSelection,
|
||||
),
|
||||
filterState: {
|
||||
...(filterState as {
|
||||
value: SelectValue;
|
||||
label?: string;
|
||||
excludeFilterValues?: boolean;
|
||||
}),
|
||||
excludeFilterValues,
|
||||
},
|
||||
});
|
||||
}, [excludeFilterValues]);
|
||||
|
||||
const handleExclusionToggle = (value: string) => {
|
||||
@@ -396,11 +352,8 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
validateStatus={filterState.validateStatus}
|
||||
extra={formItemExtra}
|
||||
>
|
||||
<StyledSpace
|
||||
appSection={appSection}
|
||||
inverseSelection={inverseSelection}
|
||||
>
|
||||
{appSection !== AppSection.FilterConfigModal && inverseSelection && (
|
||||
<StyledSpace $inverseSelection={inverseSelection}>
|
||||
{inverseSelection && (
|
||||
<Select
|
||||
className="exclude-select"
|
||||
value={`${excludeFilterValues}`}
|
||||
|
||||
@@ -26,6 +26,7 @@ const TEST_DATASOURCE = {
|
||||
columns: [],
|
||||
metrics: [],
|
||||
column_formats: {},
|
||||
currency_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '__timestamp',
|
||||
// eg. ['["ds", true]', 'ds [asc]']
|
||||
|
||||
@@ -38,6 +38,7 @@ import logger from 'src/middleware/loggerMiddleware';
|
||||
import saveModal from 'src/explore/reducers/saveModalReducer';
|
||||
import explore from 'src/explore/reducers/exploreReducer';
|
||||
import exploreDatasources from 'src/explore/reducers/datasourcesReducer';
|
||||
|
||||
import { persistSqlLabStateEnhancer } from 'src/SqlLab/middlewares/persistSqlLabStateEnhancer';
|
||||
import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
|
||||
import getInitialState from 'src/SqlLab/reducers/getInitialState';
|
||||
@@ -56,7 +57,6 @@ import { AnyDatasourcesAction } from 'src/explore/actions/datasourcesActions';
|
||||
import { HydrateExplore } from 'src/explore/actions/hydrateExplore';
|
||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||
import { Dataset } from '@superset-ui/chart-controls';
|
||||
import databaseReducer from 'src/database/reducers';
|
||||
|
||||
// Some reducers don't do anything, and redux is just used to reference the initial "state".
|
||||
// This may change later, as the client application takes on more responsibilities.
|
||||
@@ -139,7 +139,6 @@ const reducers = {
|
||||
reports,
|
||||
saveModal,
|
||||
explore,
|
||||
database: databaseReducer,
|
||||
};
|
||||
|
||||
/* In some cases the jinja template injects two separate React apps into basic.html
|
||||
|
||||
Generated
+14
-32
@@ -14,12 +14,12 @@
|
||||
"ioredis": "^4.28.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"uuid": "^11.1.0",
|
||||
"uuid": "^11.0.2",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/ioredis": "^4.27.8",
|
||||
@@ -826,11 +826,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz",
|
||||
"integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==",
|
||||
"version": "9.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
|
||||
"integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
@@ -3280,16 +3279,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/@eslint/js": {
|
||||
"version": "9.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
|
||||
"integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -6822,14 +6811,13 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
|
||||
"integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
@@ -7627,9 +7615,9 @@
|
||||
}
|
||||
},
|
||||
"@eslint/js": {
|
||||
"version": "9.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz",
|
||||
"integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==",
|
||||
"version": "9.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
|
||||
"integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
|
||||
"dev": true
|
||||
},
|
||||
"@eslint/object-schema": {
|
||||
@@ -9400,12 +9388,6 @@
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": {
|
||||
"version": "9.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
|
||||
"integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
|
||||
"dev": true
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -12025,9 +12007,9 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
|
||||
"integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ=="
|
||||
},
|
||||
"v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
"ioredis": "^4.28.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"uuid": "^11.1.0",
|
||||
"uuid": "^11.0.2",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/ioredis": "^4.27.8",
|
||||
|
||||
@@ -33,18 +33,6 @@ def get_dataset_exist_error_msg(table: Table) -> str:
|
||||
return _("Dataset %(table)s already exists", table=table)
|
||||
|
||||
|
||||
class MultiCatalogDisabledValidationError(ValidationError):
|
||||
"""
|
||||
Validation error for using a non-default catalog when multi-catalog is disabled
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
[_("Only the default catalog is supported for this connection")],
|
||||
field_name="catalog",
|
||||
)
|
||||
|
||||
|
||||
class DatabaseNotFoundValidationError(ValidationError):
|
||||
"""
|
||||
Marshmallow validation error for database does not exist
|
||||
@@ -54,6 +42,15 @@ class DatabaseNotFoundValidationError(ValidationError):
|
||||
super().__init__([_("Database does not exist")], field_name="database")
|
||||
|
||||
|
||||
class DatabaseChangeValidationError(ValidationError):
|
||||
"""
|
||||
Marshmallow validation error database changes are not allowed on update
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__([_("Database not allowed to change")], field_name="database")
|
||||
|
||||
|
||||
class DatasetExistsValidationError(ValidationError):
|
||||
"""
|
||||
Marshmallow validation error for dataset already exists
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import Counter
|
||||
from functools import partial
|
||||
@@ -28,7 +26,7 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from superset import is_feature_enabled, security_manager
|
||||
from superset.commands.base import BaseCommand, UpdateMixin
|
||||
from superset.commands.dataset.exceptions import (
|
||||
DatabaseNotFoundValidationError,
|
||||
DatabaseChangeValidationError,
|
||||
DatasetColumnNotFoundValidationError,
|
||||
DatasetColumnsDuplicateValidationError,
|
||||
DatasetColumnsExistsValidationError,
|
||||
@@ -40,13 +38,11 @@ from superset.commands.dataset.exceptions import (
|
||||
DatasetMetricsNotFoundValidationError,
|
||||
DatasetNotFoundError,
|
||||
DatasetUpdateFailedError,
|
||||
MultiCatalogDisabledValidationError,
|
||||
)
|
||||
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
|
||||
from superset.daos.dataset import DatasetDAO
|
||||
from superset.datasets.schemas import FolderSchema
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.models.core import Database
|
||||
from superset.sql_parse import Table
|
||||
from superset.utils.decorators import on_error, transaction
|
||||
|
||||
@@ -90,12 +86,38 @@ class UpdateDatasetCommand(UpdateMixin, BaseCommand):
|
||||
if not self._model:
|
||||
raise DatasetNotFoundError()
|
||||
|
||||
# Check permission to update the dataset
|
||||
# Check ownership
|
||||
try:
|
||||
security_manager.raise_for_ownership(self._model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise DatasetForbiddenError() from ex
|
||||
|
||||
database_id = self._properties.get("database")
|
||||
|
||||
catalog = self._properties.get("catalog")
|
||||
if not catalog:
|
||||
catalog = self._properties["catalog"] = (
|
||||
self._model.database.get_default_catalog()
|
||||
)
|
||||
|
||||
table = Table(
|
||||
self._properties.get("table_name"), # type: ignore
|
||||
self._properties.get("schema"),
|
||||
catalog,
|
||||
)
|
||||
|
||||
# Validate uniqueness
|
||||
if not DatasetDAO.validate_update_uniqueness(
|
||||
self._model.database,
|
||||
table,
|
||||
self._model_id,
|
||||
):
|
||||
exceptions.append(DatasetExistsValidationError(table))
|
||||
|
||||
# Validate/Populate database not allowed to change
|
||||
if database_id and database_id != self._model:
|
||||
exceptions.append(DatabaseChangeValidationError())
|
||||
|
||||
# Validate/Populate owner
|
||||
try:
|
||||
owners = self.compute_owners(
|
||||
@@ -106,68 +128,15 @@ class UpdateDatasetCommand(UpdateMixin, BaseCommand):
|
||||
except ValidationError as ex:
|
||||
exceptions.append(ex)
|
||||
|
||||
self._validate_dataset_source(exceptions)
|
||||
self._validate_semantics(exceptions)
|
||||
|
||||
if exceptions:
|
||||
raise DatasetInvalidError(exceptions=exceptions)
|
||||
|
||||
def _validate_dataset_source(self, exceptions: list[ValidationError]) -> None:
|
||||
# we know we have a valid model
|
||||
self._model = cast(SqlaTable, self._model)
|
||||
database_id = self._properties.pop("database_id", None)
|
||||
catalog = self._properties.get("catalog")
|
||||
new_db_connection: Database | None = None
|
||||
|
||||
if database_id and database_id != self._model.database.id:
|
||||
if new_db_connection := DatasetDAO.get_database_by_id(database_id):
|
||||
self._properties["database"] = new_db_connection
|
||||
else:
|
||||
exceptions.append(DatabaseNotFoundValidationError())
|
||||
db = new_db_connection or self._model.database
|
||||
default_catalog = db.get_default_catalog()
|
||||
|
||||
# If multi-catalog is disabled, and catalog provided is not
|
||||
# the default one, fail
|
||||
if (
|
||||
"catalog" in self._properties
|
||||
and catalog != default_catalog
|
||||
and not db.allow_multi_catalog
|
||||
):
|
||||
exceptions.append(MultiCatalogDisabledValidationError())
|
||||
|
||||
# If the DB connection does not support multi-catalog,
|
||||
# use the default catalog
|
||||
elif not db.allow_multi_catalog:
|
||||
catalog = self._properties["catalog"] = default_catalog
|
||||
|
||||
# Fallback to using the previous value if not provided
|
||||
elif "catalog" not in self._properties:
|
||||
catalog = self._model.catalog
|
||||
|
||||
schema = (
|
||||
self._properties["schema"]
|
||||
if "schema" in self._properties
|
||||
else self._model.schema
|
||||
)
|
||||
|
||||
table = Table(
|
||||
self._properties.get("table_name", self._model.table_name),
|
||||
schema,
|
||||
catalog,
|
||||
)
|
||||
|
||||
# Validate uniqueness
|
||||
if not DatasetDAO.validate_update_uniqueness(
|
||||
db,
|
||||
table,
|
||||
self._model_id,
|
||||
):
|
||||
exceptions.append(DatasetExistsValidationError(table))
|
||||
|
||||
def _validate_semantics(self, exceptions: list[ValidationError]) -> None:
|
||||
# we know we have a valid model
|
||||
self._model = cast(SqlaTable, self._model)
|
||||
|
||||
if columns := self._properties.get("columns"):
|
||||
self._validate_columns(columns, exceptions)
|
||||
|
||||
|
||||
@@ -65,23 +65,12 @@ class QueryContextFactory: # pylint: disable=too-few-public-methods
|
||||
|
||||
result_type = result_type or ChartDataResultType.FULL
|
||||
result_format = result_format or ChartDataResultFormat.JSON
|
||||
|
||||
# The server pagination var is extracted from form data as the
|
||||
# row limit for server pagination is more
|
||||
# This particular flag server_pagination only exists for table viz type
|
||||
server_pagination = (
|
||||
bool(form_data.get("server_pagination")) if form_data else False
|
||||
)
|
||||
|
||||
queries_ = [
|
||||
self._process_query_object(
|
||||
datasource_model_instance,
|
||||
form_data,
|
||||
self._query_object_factory.create(
|
||||
result_type,
|
||||
datasource=datasource,
|
||||
server_pagination=server_pagination,
|
||||
**query_obj,
|
||||
result_type, datasource=datasource, **query_obj
|
||||
),
|
||||
)
|
||||
for query_obj in queries
|
||||
|
||||
@@ -57,7 +57,6 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
|
||||
row_limit: int | None = None,
|
||||
time_range: str | None = None,
|
||||
time_shift: str | None = None,
|
||||
server_pagination: bool | None = None,
|
||||
**kwargs: Any,
|
||||
) -> QueryObject:
|
||||
datasource_model_instance = None
|
||||
@@ -65,12 +64,7 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
|
||||
datasource_model_instance = self._convert_to_model(datasource)
|
||||
processed_extras = self._process_extras(extras)
|
||||
result_type = kwargs.setdefault("result_type", parent_result_type)
|
||||
|
||||
# Process row limit taking server pagination into account
|
||||
row_limit = self._process_row_limit(
|
||||
row_limit, result_type, server_pagination=server_pagination
|
||||
)
|
||||
|
||||
row_limit = self._process_row_limit(row_limit, result_type)
|
||||
processed_time_range = self._process_time_range(
|
||||
time_range, kwargs.get("filters"), kwargs.get("columns")
|
||||
)
|
||||
@@ -102,27 +96,14 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
|
||||
return extras
|
||||
|
||||
def _process_row_limit(
|
||||
self,
|
||||
row_limit: int | None,
|
||||
result_type: ChartDataResultType,
|
||||
server_pagination: bool | None = None,
|
||||
self, row_limit: int | None, result_type: ChartDataResultType
|
||||
) -> int:
|
||||
"""Process row limit taking into account server pagination.
|
||||
|
||||
:param row_limit: The requested row limit
|
||||
:param result_type: The type of result being processed
|
||||
:param server_pagination: Whether server-side pagination is enabled
|
||||
:return: The processed row limit
|
||||
"""
|
||||
default_row_limit = (
|
||||
self._config["SAMPLES_ROW_LIMIT"]
|
||||
if result_type == ChartDataResultType.SAMPLES
|
||||
else self._config["ROW_LIMIT"]
|
||||
)
|
||||
return apply_max_row_limit(
|
||||
row_limit or default_row_limit,
|
||||
server_pagination=server_pagination,
|
||||
)
|
||||
return apply_max_row_limit(row_limit or default_row_limit)
|
||||
|
||||
@staticmethod
|
||||
def _process_time_range(
|
||||
|
||||
@@ -969,10 +969,6 @@ MAPBOX_API_KEY = os.environ.get("MAPBOX_API_KEY", "")
|
||||
# Maximum number of rows returned for any analytical database query
|
||||
SQL_MAX_ROW = 100000
|
||||
|
||||
# Maximum number of rows for any query with Server Pagination in Table Viz type
|
||||
TABLE_VIZ_MAX_ROW_SERVER = 500000
|
||||
|
||||
|
||||
# Maximum number of rows displayed in SQL Lab UI
|
||||
# Is set to avoid out of memory/localstorage issues in browsers. Does not affect
|
||||
# exported CSVs
|
||||
|
||||
@@ -313,6 +313,10 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin): # pylint: disable=
|
||||
def column_formats(self) -> dict[str, str | None]:
|
||||
return {m.metric_name: m.d3format for m in self.metrics if m.d3format}
|
||||
|
||||
@property
|
||||
def currency_formats(self) -> dict[str, dict[str, str | None] | None]:
|
||||
return {m.metric_name: m.currency_json for m in self.metrics if m.currency_json}
|
||||
|
||||
def add_missing_metrics(self, metrics: list[SqlMetric]) -> None:
|
||||
existing_metrics = {m.metric_name for m in self.metrics}
|
||||
for metric in metrics:
|
||||
@@ -375,6 +379,7 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin): # pylint: disable=
|
||||
"id": self.id,
|
||||
"uid": self.uid,
|
||||
"column_formats": self.column_formats,
|
||||
"currency_formats": self.currency_formats,
|
||||
"description": self.description,
|
||||
"database": self.database.data, # pylint: disable=no-member
|
||||
"default_endpoint": self.default_endpoint,
|
||||
@@ -1041,7 +1046,7 @@ class SqlMetric(AuditMixinNullable, ImportExportMixin, CertificationMixin, Model
|
||||
metric_type = Column(String(32))
|
||||
description = Column(utils.MediumText())
|
||||
d3format = Column(String(128))
|
||||
currency = Column(JSON, nullable=True)
|
||||
currency = Column(String(128))
|
||||
warning_text = Column(Text)
|
||||
table_id = Column(Integer, ForeignKey("tables.id", ondelete="CASCADE"))
|
||||
expression = Column(utils.MediumText(), nullable=False)
|
||||
@@ -1096,6 +1101,16 @@ class SqlMetric(AuditMixinNullable, ImportExportMixin, CertificationMixin, Model
|
||||
def get_perm(self) -> str | None:
|
||||
return self.perm
|
||||
|
||||
@property
|
||||
def currency_json(self) -> dict[str, str | None] | None:
|
||||
try:
|
||||
return json.loads(self.currency or "{}") or None
|
||||
except (TypeError, json.JSONDecodeError) as exc:
|
||||
logger.error(
|
||||
"Unable to load currency json: %r. Leaving empty.", exc, exc_info=True
|
||||
)
|
||||
return None
|
||||
|
||||
@property
|
||||
def data(self) -> dict[str, Any]:
|
||||
attrs = (
|
||||
|
||||
@@ -259,6 +259,7 @@ class DashboardDatasetSchema(Schema):
|
||||
id = fields.Int()
|
||||
uid = fields.Str()
|
||||
column_formats = fields.Dict()
|
||||
currency_formats = fields.Dict()
|
||||
database = fields.Nested(DatabaseSchema)
|
||||
default_endpoint = fields.String()
|
||||
filter_select = fields.Bool()
|
||||
|
||||
@@ -221,6 +221,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
||||
"datasource_name",
|
||||
"name",
|
||||
"column_formats",
|
||||
"currency_formats",
|
||||
"granularity_sqla",
|
||||
"time_grain_sqla",
|
||||
"order_by_choices",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# under the License.
|
||||
import logging
|
||||
|
||||
import pandas as pd
|
||||
import polyline
|
||||
from sqlalchemy import inspect, String, Text
|
||||
|
||||
@@ -24,7 +25,7 @@ from superset.sql_parse import Table
|
||||
from superset.utils import json
|
||||
|
||||
from ..utils.database import get_example_database
|
||||
from .helpers import get_table_connector_registry, read_example_data
|
||||
from .helpers import get_example_url, get_table_connector_registry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,9 +38,8 @@ def load_bart_lines(only_metadata: bool = False, force: bool = False) -> None:
|
||||
table_exists = database.has_table(Table(tbl_name, schema))
|
||||
|
||||
if not only_metadata and (not table_exists or force):
|
||||
df = read_example_data(
|
||||
"bart-lines.json.gz", encoding="latin-1", compression="gzip"
|
||||
)
|
||||
url = get_example_url("bart-lines.json.gz")
|
||||
df = pd.read_json(url, encoding="latin-1", compression="gzip")
|
||||
df["path_json"] = df.path.map(json.dumps)
|
||||
df["polyline"] = df.path.map(polyline.encode)
|
||||
del df["path"]
|
||||
|
||||
@@ -33,11 +33,11 @@ from superset.utils.core import DatasourceType
|
||||
|
||||
from ..utils.database import get_example_database
|
||||
from .helpers import (
|
||||
get_example_url,
|
||||
get_slice_json,
|
||||
get_table_connector_registry,
|
||||
merge_slice,
|
||||
misc_dash_slices,
|
||||
read_example_data,
|
||||
update_slice_ids,
|
||||
)
|
||||
|
||||
@@ -57,8 +57,8 @@ def gen_filter(
|
||||
|
||||
|
||||
def load_data(tbl_name: str, database: Database, sample: bool = False) -> None:
|
||||
pdf = read_example_data("birth_names2.json.gz", compression="gzip")
|
||||
|
||||
url = get_example_url("birth_names2.json.gz")
|
||||
pdf = pd.read_json(url, compression="gzip")
|
||||
# TODO(bkyryliuk): move load examples data into the pytest fixture
|
||||
if database.backend == "presto":
|
||||
pdf.ds = pd.to_datetime(pdf.ds, unit="ms")
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import BigInteger, Date, inspect, String
|
||||
from sqlalchemy.sql import column
|
||||
|
||||
@@ -28,11 +29,11 @@ from superset.sql_parse import Table
|
||||
from superset.utils.core import DatasourceType
|
||||
|
||||
from .helpers import (
|
||||
get_example_url,
|
||||
get_slice_json,
|
||||
get_table_connector_registry,
|
||||
merge_slice,
|
||||
misc_dash_slices,
|
||||
read_example_data,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -48,9 +49,8 @@ def load_country_map_data(only_metadata: bool = False, force: bool = False) -> N
|
||||
table_exists = database.has_table(Table(tbl_name, schema))
|
||||
|
||||
if not only_metadata and (not table_exists or force):
|
||||
data = read_example_data(
|
||||
"birth_france_data_for_country_map.csv", encoding="utf-8"
|
||||
)
|
||||
url = get_example_url("birth_france_data_for_country_map.csv")
|
||||
data = pd.read_csv(url, encoding="utf-8")
|
||||
data["dttm"] = datetime.datetime.now().date()
|
||||
data.to_sql(
|
||||
tbl_name,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import logging
|
||||
import textwrap
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import Float, inspect, String
|
||||
from sqlalchemy.sql import column
|
||||
|
||||
@@ -28,11 +29,11 @@ from superset.sql_parse import Table
|
||||
from superset.utils.core import DatasourceType
|
||||
|
||||
from .helpers import (
|
||||
get_example_url,
|
||||
get_slice_json,
|
||||
get_table_connector_registry,
|
||||
merge_slice,
|
||||
misc_dash_slices,
|
||||
read_example_data,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -50,7 +51,8 @@ def load_energy(
|
||||
table_exists = database.has_table(Table(tbl_name, schema))
|
||||
|
||||
if not only_metadata and (not table_exists or force):
|
||||
pdf = read_example_data("energy.json.gz", compression="gzip")
|
||||
url = get_example_url("energy.json.gz")
|
||||
pdf = pd.read_json(url, compression="gzip")
|
||||
pdf = pdf.head(100) if sample else pdf
|
||||
pdf.to_sql(
|
||||
tbl_name,
|
||||
|
||||
@@ -23,7 +23,7 @@ import superset.utils.database as database_utils
|
||||
from superset import db
|
||||
from superset.sql_parse import Table
|
||||
|
||||
from .helpers import get_table_connector_registry, read_example_data
|
||||
from .helpers import get_example_url, get_table_connector_registry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,14 +37,12 @@ def load_flights(only_metadata: bool = False, force: bool = False) -> None:
|
||||
table_exists = database.has_table(Table(tbl_name, schema))
|
||||
|
||||
if not only_metadata and (not table_exists or force):
|
||||
pdf = read_example_data(
|
||||
"flight_data.csv.gz", encoding="latin-1", compression="gzip"
|
||||
)
|
||||
flight_data_url = get_example_url("flight_data.csv.gz")
|
||||
pdf = pd.read_csv(flight_data_url, encoding="latin-1", compression="gzip")
|
||||
|
||||
# Loading airports info to join and get lat/long
|
||||
airports = read_example_data(
|
||||
"airports.csv.gz", encoding="latin-1", compression="gzip"
|
||||
)
|
||||
airports_url = get_example_url("airports.csv.gz")
|
||||
airports = pd.read_csv(airports_url, encoding="latin-1", compression="gzip")
|
||||
airports = airports.set_index("IATA_CODE")
|
||||
|
||||
pdf[ # pylint: disable=unsupported-assignment-operation,useless-suppression
|
||||
|
||||
@@ -14,81 +14,35 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Helpers for loading Superset example datasets.
|
||||
|
||||
All Superset example data files (CSV, JSON, etc.) are fetched via the
|
||||
jsDelivr CDN instead of raw.githubusercontent.com to avoid GitHub API
|
||||
rate limits (60 anonymous requests/hour/IP).
|
||||
|
||||
jsDelivr is a multi‑CDN front for public GitHub repos and supports
|
||||
arbitrary paths including nested folders. It doesn’t use the GitHub REST API
|
||||
and advertises unlimited bandwidth for open-source use.
|
||||
|
||||
Example URL::
|
||||
|
||||
https://cdn.jsdelivr.net/gh/apache-superset/examples-data@master/datasets/examples/slack/messages.csv
|
||||
|
||||
Environment knobs
|
||||
-----------------
|
||||
``SUPERSET_EXAMPLES_DATA_REF`` (default: ``master``)
|
||||
Tag / branch / SHA to pin so builds remain reproducible.
|
||||
|
||||
``SUPERSET_EXAMPLES_BASE_URL``
|
||||
Override the base completely if you want to host the files elsewhere
|
||||
(internal mirror, S3 bucket, ASF downloads, …). **Include any query
|
||||
string required by your hosting (e.g. ``?raw=true`` if you point back
|
||||
to a GitHub *blob* URL).**
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
from urllib.error import HTTPError
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from superset import app, db
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
from superset.models.slice import Slice
|
||||
from superset.utils import json
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public sample‑data mirror configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
BASE_COMMIT: str = os.getenv("SUPERSET_EXAMPLES_DATA_REF", "master")
|
||||
BASE_URL: str = os.getenv(
|
||||
"SUPERSET_EXAMPLES_BASE_URL",
|
||||
f"https://cdn.jsdelivr.net/gh/apache-superset/examples-data@{BASE_COMMIT}/",
|
||||
)
|
||||
BASE_URL = "https://github.com/apache-superset/examples-data/blob/master/"
|
||||
|
||||
# Slices assembled into a 'Misc Chart' dashboard
|
||||
misc_dash_slices: set[str] = set()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Utility functions
|
||||
# ---------------------------------------------------------------------------
|
||||
misc_dash_slices: set[str] = set() # slices assembled in a 'Misc Chart' dashboard
|
||||
|
||||
|
||||
def get_table_connector_registry() -> Any:
|
||||
"""Return the SqlaTable registry so we can mock it in unit tests."""
|
||||
return SqlaTable
|
||||
|
||||
|
||||
def get_examples_folder() -> str:
|
||||
"""Return local path to the examples folder (when vendored)."""
|
||||
return os.path.join(app.config["BASE_DIR"], "examples")
|
||||
|
||||
|
||||
def update_slice_ids(pos: dict[Any, Any]) -> list[Slice]:
|
||||
"""Update slice ids in ``position_json`` and return the slices found."""
|
||||
"""Update slice ids in position_json and return the slices found."""
|
||||
slice_components = [
|
||||
component
|
||||
for component in pos.values()
|
||||
if isinstance(component, dict) and component.get("type") == "CHART"
|
||||
]
|
||||
slices: dict[str, Slice] = {}
|
||||
slices = {}
|
||||
for name in {component["meta"]["sliceName"] for component in slice_components}:
|
||||
slc = db.session.query(Slice).filter_by(slice_name=name).first()
|
||||
if slc:
|
||||
@@ -102,54 +56,17 @@ def update_slice_ids(pos: dict[Any, Any]) -> list[Slice]:
|
||||
|
||||
|
||||
def merge_slice(slc: Slice) -> None:
|
||||
"""Upsert a Slice by name."""
|
||||
existing = db.session.query(Slice).filter_by(slice_name=slc.slice_name).first()
|
||||
if existing:
|
||||
db.session.delete(existing)
|
||||
o = db.session.query(Slice).filter_by(slice_name=slc.slice_name).first()
|
||||
if o:
|
||||
db.session.delete(o)
|
||||
db.session.add(slc)
|
||||
|
||||
|
||||
def get_slice_json(defaults: dict[Any, Any], **kwargs: Any) -> str:
|
||||
"""Return JSON string for a chart definition, merging extra kwargs."""
|
||||
defaults_copy = defaults.copy()
|
||||
defaults_copy.update(kwargs)
|
||||
return json.dumps(defaults_copy, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
def get_example_url(filepath: str) -> str:
|
||||
"""Return an absolute URL to *filepath* under the examples‑data repo.
|
||||
|
||||
All calls are routed through jsDelivr unless overridden. Supports nested
|
||||
paths like ``datasets/examples/slack/messages.csv``.
|
||||
"""
|
||||
return f"{BASE_URL}{filepath}"
|
||||
|
||||
|
||||
def read_example_data(
|
||||
filepath: str,
|
||||
max_attempts: int = 5,
|
||||
wait_seconds: float = 60,
|
||||
**kwargs: Any,
|
||||
) -> pd.DataFrame:
|
||||
"""Load CSV or JSON from example data mirror with retry/backoff."""
|
||||
from superset.examples.helpers import get_example_url
|
||||
|
||||
url = get_example_url(filepath)
|
||||
is_json = filepath.endswith(".json") or filepath.endswith(".json.gz")
|
||||
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
try:
|
||||
if is_json:
|
||||
return pd.read_json(url, **kwargs)
|
||||
return pd.read_csv(url, **kwargs)
|
||||
except HTTPError as e:
|
||||
if e.code == 429 and attempt < max_attempts:
|
||||
sleep_time = wait_seconds * (2 ** (attempt - 1))
|
||||
print(
|
||||
f"HTTP 429 received from {url}. ",
|
||||
f"Retrying in {sleep_time:.1f}s ",
|
||||
f"(attempt {attempt}/{max_attempts})...",
|
||||
)
|
||||
time.sleep(sleep_time)
|
||||
else:
|
||||
raise
|
||||
return f"{BASE_URL}{filepath}?raw=true"
|
||||
|
||||
@@ -19,6 +19,7 @@ import logging
|
||||
import random
|
||||
|
||||
import geohash
|
||||
import pandas as pd
|
||||
from sqlalchemy import DateTime, Float, inspect, String
|
||||
|
||||
import superset.utils.database as database_utils
|
||||
@@ -28,11 +29,11 @@ from superset.sql_parse import Table
|
||||
from superset.utils.core import DatasourceType
|
||||
|
||||
from .helpers import (
|
||||
get_example_url,
|
||||
get_slice_json,
|
||||
get_table_connector_registry,
|
||||
merge_slice,
|
||||
misc_dash_slices,
|
||||
read_example_data,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -47,9 +48,8 @@ def load_long_lat_data(only_metadata: bool = False, force: bool = False) -> None
|
||||
table_exists = database.has_table(Table(tbl_name, schema))
|
||||
|
||||
if not only_metadata and (not table_exists or force):
|
||||
pdf = read_example_data(
|
||||
"san_francisco.csv.gz", encoding="utf-8", compression="gzip"
|
||||
)
|
||||
url = get_example_url("san_francisco.csv.gz")
|
||||
pdf = pd.read_csv(url, encoding="utf-8", compression="gzip")
|
||||
start = datetime.datetime.now().replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
|
||||
@@ -27,11 +27,11 @@ from superset.utils.core import DatasourceType
|
||||
|
||||
from ..utils.database import get_example_database
|
||||
from .helpers import (
|
||||
get_example_url,
|
||||
get_slice_json,
|
||||
get_table_connector_registry,
|
||||
merge_slice,
|
||||
misc_dash_slices,
|
||||
read_example_data,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -48,10 +48,8 @@ def load_multiformat_time_series( # pylint: disable=too-many-locals
|
||||
table_exists = database.has_table(Table(tbl_name, schema))
|
||||
|
||||
if not only_metadata and (not table_exists or force):
|
||||
pdf = read_example_data(
|
||||
"multiformat_time_series.json.gz", compression="gzip"
|
||||
)
|
||||
|
||||
url = get_example_url("multiformat_time_series.json.gz")
|
||||
pdf = pd.read_json(url, compression="gzip")
|
||||
# TODO(bkyryliuk): move load examples data into the pytest fixture
|
||||
if database.backend == "presto":
|
||||
pdf.ds = pd.to_datetime(pdf.ds, unit="s")
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import inspect, String, Text
|
||||
|
||||
import superset.utils.database as database_utils
|
||||
@@ -24,7 +25,7 @@ from superset import db
|
||||
from superset.sql_parse import Table
|
||||
from superset.utils import json
|
||||
|
||||
from .helpers import get_table_connector_registry, read_example_data
|
||||
from .helpers import get_example_url, get_table_connector_registry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,7 +38,8 @@ def load_paris_iris_geojson(only_metadata: bool = False, force: bool = False) ->
|
||||
table_exists = database.has_table(Table(tbl_name, schema))
|
||||
|
||||
if not only_metadata and (not table_exists or force):
|
||||
df = read_example_data("paris_iris.json.gz", compression="gzip")
|
||||
url = get_example_url("paris_iris.json.gz")
|
||||
df = pd.read_json(url, compression="gzip")
|
||||
df["features"] = df.features.map(json.dumps)
|
||||
|
||||
df.to_sql(
|
||||
|
||||
@@ -26,10 +26,10 @@ from superset.sql_parse import Table
|
||||
from superset.utils.core import DatasourceType
|
||||
|
||||
from .helpers import (
|
||||
get_example_url,
|
||||
get_slice_json,
|
||||
get_table_connector_registry,
|
||||
merge_slice,
|
||||
read_example_data,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -46,7 +46,8 @@ def load_random_time_series_data(
|
||||
table_exists = database.has_table(Table(tbl_name, schema))
|
||||
|
||||
if not only_metadata and (not table_exists or force):
|
||||
pdf = read_example_data("random_time_series.json.gz", compression="gzip")
|
||||
url = get_example_url("random_time_series.json.gz")
|
||||
pdf = pd.read_json(url, compression="gzip")
|
||||
if database.backend == "presto":
|
||||
pdf.ds = pd.to_datetime(pdf.ds, unit="s")
|
||||
pdf.ds = pdf.ds.dt.strftime("%Y-%m-%d %H:%M%:%S")
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import BigInteger, Float, inspect, Text
|
||||
|
||||
import superset.utils.database as database_utils
|
||||
@@ -24,7 +25,7 @@ from superset import db
|
||||
from superset.sql_parse import Table
|
||||
from superset.utils import json
|
||||
|
||||
from .helpers import get_table_connector_registry, read_example_data
|
||||
from .helpers import get_example_url, get_table_connector_registry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,7 +40,8 @@ def load_sf_population_polygons(
|
||||
table_exists = database.has_table(Table(tbl_name, schema))
|
||||
|
||||
if not only_metadata and (not table_exists or force):
|
||||
df = read_example_data("sf_population.json.gz", compression="gzip")
|
||||
url = get_example_url("sf_population.json.gz")
|
||||
df = pd.read_json(url, compression="gzip")
|
||||
df["contour"] = df.contour.map(json.dumps)
|
||||
|
||||
df.to_sql(
|
||||
|
||||
@@ -25,12 +25,12 @@ import superset.utils.database
|
||||
from superset import app, db
|
||||
from superset.connectors.sqla.models import BaseDatasource, SqlMetric
|
||||
from superset.examples.helpers import (
|
||||
get_example_url,
|
||||
get_examples_folder,
|
||||
get_slice_json,
|
||||
get_table_connector_registry,
|
||||
merge_slice,
|
||||
misc_dash_slices,
|
||||
read_example_data,
|
||||
update_slice_ids,
|
||||
)
|
||||
from superset.models.dashboard import Dashboard
|
||||
@@ -55,7 +55,8 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals
|
||||
table_exists = database.has_table(Table(tbl_name, schema))
|
||||
|
||||
if not only_metadata and (not table_exists or force):
|
||||
pdf = read_example_data("countries.json.gz", compression="gzip")
|
||||
url = get_example_url("countries.json.gz")
|
||||
pdf = pd.read_json(url, compression="gzip")
|
||||
pdf.columns = [col.replace(".", "_") for col in pdf.columns]
|
||||
if database.backend == "presto":
|
||||
pdf.year = pd.to_datetime(pdf.year)
|
||||
|
||||
@@ -25,6 +25,7 @@ class DatasetSchema(Schema):
|
||||
}
|
||||
)
|
||||
column_formats = fields.Dict(metadata={"description": "Column formats."})
|
||||
currency_formats = fields.Dict(metadata={"description": "Currency formats."})
|
||||
columns = fields.List(fields.Dict(), metadata={"description": "Columns metadata."})
|
||||
database = fields.Dict(
|
||||
metadata={"description": "Database associated with the dataset."}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user