mirror of
https://github.com/apache/superset.git
synced 2026-07-07 15:25:36 +00:00
Compare commits
1 Commits
fix_exampl
...
docs_opena
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c778c15259 |
2
.github/workflows/superset-docs-deploy.yml
vendored
2
.github/workflows/superset-docs-deploy.yml
vendored
@@ -41,6 +41,8 @@ jobs:
|
|||||||
node-version-file: './docs/.nvmrc'
|
node-version-file: './docs/.nvmrc'
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: ./.github/actions/setup-backend/
|
uses: ./.github/actions/setup-backend/
|
||||||
|
- name: Update openapi docs
|
||||||
|
run: superset update_api_docs
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
|
|||||||
@@ -302,15 +302,6 @@ AUTH_USER_REGISTRATION = True
|
|||||||
AUTH_USER_REGISTRATION_ROLE = "Public"
|
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
|
Then, create a `CustomSsoSecurityManager` that extends `SupersetSecurityManager` and overrides
|
||||||
`oauth_user_info`:
|
`oauth_user_info`:
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"@emotion/styled": "^10.0.27",
|
"@emotion/styled": "^10.0.27",
|
||||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||||
"@superset-ui/style": "^0.14.23",
|
"@superset-ui/style": "^0.14.23",
|
||||||
"antd": "^5.24.9",
|
"antd": "^5.24.5",
|
||||||
"docusaurus-plugin-less": "^2.0.2",
|
"docusaurus-plugin-less": "^2.0.2",
|
||||||
"less": "^4.3.0",
|
"less": "^4.3.0",
|
||||||
"less-loader": "^11.0.0",
|
"less-loader": "^11.0.0",
|
||||||
|
|||||||
102
docs/yarn.lock
102
docs/yarn.lock
@@ -1092,13 +1092,20 @@
|
|||||||
core-js-pure "^3.30.2"
|
core-js-pure "^3.30.2"
|
||||||
regenerator-runtime "^0.14.0"
|
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"
|
version "7.27.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762"
|
||||||
integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==
|
integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.14.0"
|
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":
|
"@babel/template@^7.25.9", "@babel/template@^7.26.9", "@babel/template@^7.27.0":
|
||||||
version "7.27.0"
|
version "7.27.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4"
|
||||||
@@ -4179,10 +4186,10 @@ ansi-styles@^6.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||||
|
|
||||||
antd@^5.24.9:
|
antd@^5.24.5:
|
||||||
version "5.24.9"
|
version "5.24.5"
|
||||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.24.9.tgz#c5862e02ed770bd95e312961f4f0b7b158a004d9"
|
resolved "https://registry.yarnpkg.com/antd/-/antd-5.24.5.tgz#b0976a113163888d1477f9e666c3c23352b098e9"
|
||||||
integrity sha512-liB+Y/JwD5/KSKbK1Z1EVAbWcoWYvWJ1s97AbbT+mOdigpJQuWwH7kG8IXNEljI7onvj0DdD43TXhSRLUu9AMA==
|
integrity sha512-1lAv/G+9ewQanyoAo3JumQmIlVxwo5QwWGb6QCHYc40Cq0NxC/EzITcjsgq1PSaTUpLkKq8A2l7Fjtu47vqQBg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ant-design/colors" "^7.2.0"
|
"@ant-design/colors" "^7.2.0"
|
||||||
"@ant-design/cssinjs" "^1.23.0"
|
"@ant-design/cssinjs" "^1.23.0"
|
||||||
@@ -4206,13 +4213,13 @@ antd@^5.24.9:
|
|||||||
rc-drawer "~7.2.0"
|
rc-drawer "~7.2.0"
|
||||||
rc-dropdown "~4.2.1"
|
rc-dropdown "~4.2.1"
|
||||||
rc-field-form "~2.7.0"
|
rc-field-form "~2.7.0"
|
||||||
rc-image "~7.12.0"
|
rc-image "~7.11.1"
|
||||||
rc-input "~1.8.0"
|
rc-input "~1.7.3"
|
||||||
rc-input-number "~9.5.0"
|
rc-input-number "~9.4.0"
|
||||||
rc-mentions "~2.20.0"
|
rc-mentions "~2.19.1"
|
||||||
rc-menu "~9.16.1"
|
rc-menu "~9.16.1"
|
||||||
rc-motion "^2.9.5"
|
rc-motion "^2.9.5"
|
||||||
rc-notification "~5.6.4"
|
rc-notification "~5.6.3"
|
||||||
rc-pagination "~5.1.0"
|
rc-pagination "~5.1.0"
|
||||||
rc-picker "~4.11.3"
|
rc-picker "~4.11.3"
|
||||||
rc-progress "~4.0.0"
|
rc-progress "~4.0.0"
|
||||||
@@ -4224,8 +4231,8 @@ antd@^5.24.9:
|
|||||||
rc-steps "~6.0.1"
|
rc-steps "~6.0.1"
|
||||||
rc-switch "~4.1.0"
|
rc-switch "~4.1.0"
|
||||||
rc-table "~7.50.4"
|
rc-table "~7.50.4"
|
||||||
rc-tabs "~15.6.1"
|
rc-tabs "~15.5.1"
|
||||||
rc-textarea "~1.10.0"
|
rc-textarea "~1.9.0"
|
||||||
rc-tooltip "~6.4.0"
|
rc-tooltip "~6.4.0"
|
||||||
rc-tree "~5.13.1"
|
rc-tree "~5.13.1"
|
||||||
rc-tree-select "~5.27.0"
|
rc-tree-select "~5.27.0"
|
||||||
@@ -5667,7 +5674,12 @@ data-view-byte-offset@^1.0.1:
|
|||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
is-data-view "^1.0.1"
|
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"
|
version "1.11.13"
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
|
||||||
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
|
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
|
||||||
@@ -10676,10 +10688,10 @@ rc-field-form@~2.7.0:
|
|||||||
"@rc-component/async-validator" "^5.0.3"
|
"@rc-component/async-validator" "^5.0.3"
|
||||||
rc-util "^5.32.2"
|
rc-util "^5.32.2"
|
||||||
|
|
||||||
rc-image@~7.12.0:
|
rc-image@~7.11.1:
|
||||||
version "7.12.0"
|
version "7.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-7.12.0.tgz#95e9314701e668217d113c1f29b4f01ac025cafe"
|
resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-7.11.1.tgz#3ab290708dc053d3681de94186522e4e594f6772"
|
||||||
integrity sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==
|
integrity sha512-XuoWx4KUXg7hNy5mRTy1i8c8p3K8boWg6UajbHpDXS5AlRVucNfTi5YxTtPBTBzegxAZpvuLfh3emXFt6ybUdA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.11.2"
|
"@babel/runtime" "^7.11.2"
|
||||||
"@rc-component/portal" "^1.0.2"
|
"@rc-component/portal" "^1.0.2"
|
||||||
@@ -10688,37 +10700,37 @@ rc-image@~7.12.0:
|
|||||||
rc-motion "^2.6.2"
|
rc-motion "^2.6.2"
|
||||||
rc-util "^5.34.1"
|
rc-util "^5.34.1"
|
||||||
|
|
||||||
rc-input-number@~9.5.0:
|
rc-input-number@~9.4.0:
|
||||||
version "9.5.0"
|
version "9.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-9.5.0.tgz#b47963d0f2cbd85ab2f1badfdc089a904c073f38"
|
resolved "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.4.0.tgz"
|
||||||
integrity sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==
|
integrity sha512-Tiy4DcXcFXAf9wDhN8aUAyMeCLHJUHA/VA/t7Hj8ZEx5ETvxG7MArDOSE6psbiSCo+vJPm4E3fGN710ITVn6GA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.10.1"
|
"@babel/runtime" "^7.10.1"
|
||||||
"@rc-component/mini-decimal" "^1.0.1"
|
"@rc-component/mini-decimal" "^1.0.1"
|
||||||
classnames "^2.2.5"
|
classnames "^2.2.5"
|
||||||
rc-input "~1.8.0"
|
rc-input "~1.7.1"
|
||||||
rc-util "^5.40.1"
|
rc-util "^5.40.1"
|
||||||
|
|
||||||
rc-input@~1.8.0:
|
rc-input@~1.7.1, rc-input@~1.7.3:
|
||||||
version "1.8.0"
|
version "1.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-1.8.0.tgz#d2f4404befebf2fbdc28390d5494c302f74ae974"
|
resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-1.7.3.tgz#cb334a17b93ce985bceb243b4c111a5ed641e0e3"
|
||||||
integrity sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==
|
integrity sha512-A5w4egJq8+4JzlQ55FfQjDnPvOaAbzwC3VLOAdOytyek3TboSOP9qxN+Gifup+shVXfvecBLBbWBpWxmk02SWQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.11.1"
|
"@babel/runtime" "^7.11.1"
|
||||||
classnames "^2.2.1"
|
classnames "^2.2.1"
|
||||||
rc-util "^5.18.1"
|
rc-util "^5.18.1"
|
||||||
|
|
||||||
rc-mentions@~2.20.0:
|
rc-mentions@~2.19.1:
|
||||||
version "2.20.0"
|
version "2.19.1"
|
||||||
resolved "https://registry.yarnpkg.com/rc-mentions/-/rc-mentions-2.20.0.tgz#3bbeac0352b02e0ce3e1244adb48701bb6903bf7"
|
resolved "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.19.1.tgz"
|
||||||
integrity sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==
|
integrity sha512-KK3bAc/bPFI993J3necmaMXD2reZTzytZdlTvkeBbp50IGH1BDPDvxLdHDUrpQx2b2TGaVJsn+86BvYa03kGqA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.22.5"
|
"@babel/runtime" "^7.22.5"
|
||||||
"@rc-component/trigger" "^2.0.0"
|
"@rc-component/trigger" "^2.0.0"
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
rc-input "~1.8.0"
|
rc-input "~1.7.1"
|
||||||
rc-menu "~9.16.0"
|
rc-menu "~9.16.0"
|
||||||
rc-textarea "~1.10.0"
|
rc-textarea "~1.9.0"
|
||||||
rc-util "^5.34.1"
|
rc-util "^5.34.1"
|
||||||
|
|
||||||
rc-menu@~9.16.0, rc-menu@~9.16.1:
|
rc-menu@~9.16.0, rc-menu@~9.16.1:
|
||||||
@@ -10742,10 +10754,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"
|
classnames "^2.2.1"
|
||||||
rc-util "^5.44.0"
|
rc-util "^5.44.0"
|
||||||
|
|
||||||
rc-notification@~5.6.4:
|
rc-notification@~5.6.3:
|
||||||
version "5.6.4"
|
version "5.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/rc-notification/-/rc-notification-5.6.4.tgz#ea89c39c13cd517fdfd97fe63f03376fabb78544"
|
resolved "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.3.tgz"
|
||||||
integrity sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==
|
integrity sha512-42szwnn8VYQoT6GnjO00i1iwqV9D1TTMvxObWsuLwgl0TsOokzhkYiufdtQBsJMFjJravS1hfDKVMHLKLcPE4g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.10.1"
|
"@babel/runtime" "^7.10.1"
|
||||||
classnames "2.x"
|
classnames "2.x"
|
||||||
@@ -10873,10 +10885,10 @@ rc-table@~7.50.4:
|
|||||||
rc-util "^5.44.3"
|
rc-util "^5.44.3"
|
||||||
rc-virtual-list "^3.14.2"
|
rc-virtual-list "^3.14.2"
|
||||||
|
|
||||||
rc-tabs@~15.6.1:
|
rc-tabs@~15.5.1:
|
||||||
version "15.6.1"
|
version "15.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-15.6.1.tgz#f0b6c65384dfa09a64eb539e86a0667c7a650708"
|
resolved "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.1.tgz"
|
||||||
integrity sha512-/HzDV1VqOsUWyuC0c6AkxVYFjvx9+rFPKZ32ejxX0Uc7QCzcEjTA9/xMgv4HemPKwzBNX8KhGVbbumDjnj92aA==
|
integrity sha512-yiWivLAjEo5d1v2xlseB2dQocsOhkoVSfo1krS8v8r+02K+TBUjSjXIf7dgyVSxp6wRIPv5pMi5hanNUlQMgUA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.11.2"
|
"@babel/runtime" "^7.11.2"
|
||||||
classnames "2.x"
|
classnames "2.x"
|
||||||
@@ -10886,14 +10898,14 @@ rc-tabs@~15.6.1:
|
|||||||
rc-resize-observer "^1.0.0"
|
rc-resize-observer "^1.0.0"
|
||||||
rc-util "^5.34.1"
|
rc-util "^5.34.1"
|
||||||
|
|
||||||
rc-textarea@~1.10.0:
|
rc-textarea@~1.9.0:
|
||||||
version "1.10.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.0.tgz#f8f962ef83be0b8e35db97cf03dbfb86ddd9c46c"
|
resolved "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.9.0.tgz"
|
||||||
integrity sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==
|
integrity sha512-dQW/Bc/MriPBTugj2Kx9PMS5eXCCGn2cxoIaichjbNvOiARlaHdI99j4DTxLl/V8+PIfW06uFy7kjfUIDDKyxQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.10.1"
|
"@babel/runtime" "^7.10.1"
|
||||||
classnames "^2.2.1"
|
classnames "^2.2.1"
|
||||||
rc-input "~1.8.0"
|
rc-input "~1.7.1"
|
||||||
rc-resize-observer "^1.0.0"
|
rc-resize-observer "^1.0.0"
|
||||||
rc-util "^5.27.0"
|
rc-util "^5.27.0"
|
||||||
|
|
||||||
|
|||||||
@@ -252,215 +252,4 @@ describe('Visualization > Table', () => {
|
|||||||
});
|
});
|
||||||
cy.get('td').contains(/\d*%/);
|
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
import { ColumnMeta, SortSeriesData, SortSeriesType } from './types';
|
import { ColumnMeta, SortSeriesData, SortSeriesType } from './types';
|
||||||
|
|
||||||
export const DEFAULT_MAX_ROW = 100000;
|
export const DEFAULT_MAX_ROW = 100000;
|
||||||
export const DEFAULT_MAX_ROW_TABLE_SERVER = 500000;
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export const TIME_FILTER_LABELS = {
|
export const TIME_FILTER_LABELS = {
|
||||||
|
|||||||
@@ -25,4 +25,3 @@ export { default as validateNonEmpty } from './validateNonEmpty';
|
|||||||
export { default as validateMaxValue } from './validateMaxValue';
|
export { default as validateMaxValue } from './validateMaxValue';
|
||||||
export { default as validateMapboxStylesUrl } from './validateMapboxStylesUrl';
|
export { default as validateMapboxStylesUrl } from './validateMapboxStylesUrl';
|
||||||
export { default as validateTimeComparisonRangeValues } from './validateTimeComparisonRangeValues';
|
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;
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
@@ -36,25 +36,13 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import { useOverflowDetection } from './useOverflowDetection';
|
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`
|
const NumbersContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 12px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
|
const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
|
||||||
@@ -85,8 +73,6 @@ export default function PopKPI(props: PopKPIProps) {
|
|||||||
prevNumber,
|
prevNumber,
|
||||||
valueDifference,
|
valueDifference,
|
||||||
percentDifferenceFormattedString,
|
percentDifferenceFormattedString,
|
||||||
metricName,
|
|
||||||
metricNameFontSize,
|
|
||||||
headerFontSize,
|
headerFontSize,
|
||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
comparisonColorEnabled,
|
comparisonColorEnabled,
|
||||||
@@ -98,8 +84,8 @@ export default function PopKPI(props: PopKPIProps) {
|
|||||||
subtitle,
|
subtitle,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
dashboardTimeRange,
|
dashboardTimeRange,
|
||||||
showMetricName,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [comparisonRange, setComparisonRange] = useState<string>('');
|
const [comparisonRange, setComparisonRange] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -274,16 +260,9 @@ export default function PopKPI(props: PopKPIProps) {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
overflow: auto;
|
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{showMetricName && metricName && (
|
|
||||||
<MetricNameText metricNameFontSize={metricNameFontSize}>
|
|
||||||
{metricName}
|
|
||||||
</MetricNameText>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div css={bigValueContainerStyles}>
|
<div css={bigValueContainerStyles}>
|
||||||
{bigNumber}
|
{bigNumber}
|
||||||
{percentDifferenceNumber !== 0 && (
|
{percentDifferenceNumber !== 0 && (
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import {
|
|||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
subtitleControl,
|
subtitleControl,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
showMetricNameControl,
|
|
||||||
metricNameFontSizeWithVisibility,
|
|
||||||
} from '../sharedControls';
|
} from '../sharedControls';
|
||||||
import { ColorSchemeEnum } from './types';
|
import { ColorSchemeEnum } from './types';
|
||||||
|
|
||||||
@@ -72,8 +70,6 @@ const config: ControlPanelConfig = {
|
|||||||
],
|
],
|
||||||
[subtitleControl],
|
[subtitleControl],
|
||||||
[subtitleFontSize],
|
[subtitleFontSize],
|
||||||
[showMetricNameControl],
|
|
||||||
[metricNameFontSizeWithVisibility],
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
...subheaderFontSize,
|
...subheaderFontSize,
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export default class PopKPIPlugin extends ChartPlugin {
|
|||||||
tags: [
|
tags: [
|
||||||
t('Comparison'),
|
t('Comparison'),
|
||||||
t('Business'),
|
t('Business'),
|
||||||
t('ECharts'),
|
|
||||||
t('Percentages'),
|
t('Percentages'),
|
||||||
t('Report'),
|
t('Report'),
|
||||||
t('Advanced-Analytics'),
|
t('Advanced-Analytics'),
|
||||||
|
|||||||
@@ -26,13 +26,7 @@ import {
|
|||||||
SimpleAdhocFilter,
|
SimpleAdhocFilter,
|
||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import {
|
import { getComparisonFontSize, getHeaderFontSize } from './utils';
|
||||||
getComparisonFontSize,
|
|
||||||
getHeaderFontSize,
|
|
||||||
getMetricNameFontSize,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
import { getOriginalLabel } from '../utils';
|
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@@ -89,7 +83,6 @@ export default function transformProps(chartProps: ChartProps) {
|
|||||||
headerFontSize,
|
headerFontSize,
|
||||||
headerText,
|
headerText,
|
||||||
metric,
|
metric,
|
||||||
metricNameFontSize,
|
|
||||||
yAxisFormat,
|
yAxisFormat,
|
||||||
currencyFormat,
|
currencyFormat,
|
||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
@@ -98,14 +91,11 @@ export default function transformProps(chartProps: ChartProps) {
|
|||||||
percentDifferenceFormat,
|
percentDifferenceFormat,
|
||||||
subtitle = '',
|
subtitle = '',
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
columnConfig = {},
|
columnConfig,
|
||||||
} = formData;
|
} = formData;
|
||||||
const { data: dataA = [] } = queriesData[0];
|
const { data: dataA = [] } = queriesData[0];
|
||||||
const data = dataA;
|
const data = dataA;
|
||||||
const metricName = metric ? getMetricLabel(metric) : '';
|
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 timeComparison = ensureIsArray(chartProps.rawFormData?.time_compare)[0];
|
||||||
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
||||||
const currentTimeRangeFilter = chartProps.rawFormData?.adhoc_filters?.filter(
|
const currentTimeRangeFilter = chartProps.rawFormData?.adhoc_filters?.filter(
|
||||||
@@ -189,7 +179,7 @@ export default function transformProps(chartProps: ChartProps) {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
data,
|
data,
|
||||||
metricName: originalLabel,
|
metricName,
|
||||||
bigNumber,
|
bigNumber,
|
||||||
prevNumber,
|
prevNumber,
|
||||||
valueDifference,
|
valueDifference,
|
||||||
@@ -197,8 +187,6 @@ export default function transformProps(chartProps: ChartProps) {
|
|||||||
boldText,
|
boldText,
|
||||||
subtitle,
|
subtitle,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
showMetricName,
|
|
||||||
metricNameFontSize: getMetricNameFontSize(metricNameFontSize),
|
|
||||||
headerFontSize: getHeaderFontSize(headerFontSize),
|
headerFontSize: getHeaderFontSize(headerFontSize),
|
||||||
subheaderFontSize: getComparisonFontSize(subheaderFontSize),
|
subheaderFontSize: getComparisonFontSize(subheaderFontSize),
|
||||||
headerText,
|
headerText,
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ export type PopKPIProps = PopKPIStylesProps &
|
|||||||
data: TimeseriesDataRecord[];
|
data: TimeseriesDataRecord[];
|
||||||
metrics: Metric[];
|
metrics: Metric[];
|
||||||
metricName: string;
|
metricName: string;
|
||||||
metricNameFontSize?: number;
|
|
||||||
showMetricName: boolean;
|
|
||||||
bigNumber: string;
|
bigNumber: string;
|
||||||
prevNumber: string;
|
prevNumber: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
|
|||||||
@@ -16,19 +16,10 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import {
|
import { headerFontSize, subheaderFontSize } from '../sharedControls';
|
||||||
headerFontSize,
|
|
||||||
subheaderFontSize,
|
|
||||||
metricNameFontSize,
|
|
||||||
} from '../sharedControls';
|
|
||||||
|
|
||||||
const headerFontSizes = [16, 20, 30, 48, 60];
|
const headerFontSizes = [16, 20, 30, 48, 60];
|
||||||
const sharedFontSizes = [16, 20, 26, 32, 40];
|
const comparisonFontSizes = [16, 20, 26, 32, 40];
|
||||||
|
|
||||||
const metricNameProportionValues =
|
|
||||||
metricNameFontSize.config.options.map(
|
|
||||||
(option: { label: string; value: number }) => option.value,
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
const headerProportionValues =
|
const headerProportionValues =
|
||||||
headerFontSize.config.options.map(
|
headerFontSize.config.options.map(
|
||||||
@@ -49,10 +40,6 @@ const getFontSizeMapping = (
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const metricNameFontSizesMapping = getFontSizeMapping(
|
|
||||||
metricNameProportionValues,
|
|
||||||
sharedFontSizes,
|
|
||||||
);
|
|
||||||
const headerFontSizesMapping = getFontSizeMapping(
|
const headerFontSizesMapping = getFontSizeMapping(
|
||||||
headerProportionValues,
|
headerProportionValues,
|
||||||
headerFontSizes,
|
headerFontSizes,
|
||||||
@@ -60,17 +47,13 @@ const headerFontSizesMapping = getFontSizeMapping(
|
|||||||
|
|
||||||
const comparisonFontSizesMapping = getFontSizeMapping(
|
const comparisonFontSizesMapping = getFontSizeMapping(
|
||||||
subheaderProportionValues,
|
subheaderProportionValues,
|
||||||
sharedFontSizes,
|
comparisonFontSizes,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getMetricNameFontSize = (proportionValue: number) =>
|
|
||||||
metricNameFontSizesMapping[proportionValue] ??
|
|
||||||
sharedFontSizes[sharedFontSizes.length - 1];
|
|
||||||
|
|
||||||
export const getHeaderFontSize = (proportionValue: number) =>
|
export const getHeaderFontSize = (proportionValue: number) =>
|
||||||
headerFontSizesMapping[proportionValue] ??
|
headerFontSizesMapping[proportionValue] ??
|
||||||
headerFontSizes[headerFontSizes.length - 1];
|
headerFontSizes[headerFontSizes.length - 1];
|
||||||
|
|
||||||
export const getComparisonFontSize = (proportionValue: number) =>
|
export const getComparisonFontSize = (proportionValue: number) =>
|
||||||
comparisonFontSizesMapping[proportionValue] ??
|
comparisonFontSizesMapping[proportionValue] ??
|
||||||
sharedFontSizes[sharedFontSizes.length - 1];
|
comparisonFontSizes[comparisonFontSizes.length - 1];
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import {
|
|||||||
headerFontSize,
|
headerFontSize,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
subtitleControl,
|
subtitleControl,
|
||||||
showMetricNameControl,
|
|
||||||
metricNameFontSizeWithVisibility,
|
|
||||||
} from '../sharedControls';
|
} from '../sharedControls';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -46,8 +44,6 @@ export default {
|
|||||||
[headerFontSize],
|
[headerFontSize],
|
||||||
[subtitleControl],
|
[subtitleControl],
|
||||||
[subtitleFontSize],
|
[subtitleFontSize],
|
||||||
[showMetricNameControl],
|
|
||||||
[metricNameFontSizeWithVisibility],
|
|
||||||
['y_axis_format'],
|
['y_axis_format'],
|
||||||
['currency_format'],
|
['currency_format'],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ const metadata = {
|
|||||||
tags: [
|
tags: [
|
||||||
t('Additive'),
|
t('Additive'),
|
||||||
t('Business'),
|
t('Business'),
|
||||||
t('ECharts'),
|
|
||||||
t('Legacy'),
|
t('Legacy'),
|
||||||
t('Percentages'),
|
t('Percentages'),
|
||||||
t('Featured'),
|
t('Featured'),
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ jest.mock('@superset-ui/core', () => ({
|
|||||||
jest.mock('../utils', () => ({
|
jest.mock('../utils', () => ({
|
||||||
getDateFormatter: jest.fn(() => (v: any) => `${v}pm`),
|
getDateFormatter: jest.fn(() => (v: any) => `${v}pm`),
|
||||||
parseMetricValue: jest.fn(val => Number(val)),
|
parseMetricValue: jest.fn(val => Number(val)),
|
||||||
getOriginalLabel: jest.fn((metric, metrics) => metric),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('BigNumberTotal transformProps', () => {
|
describe('BigNumberTotal transformProps', () => {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
getValueFormatter,
|
getValueFormatter,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { BigNumberTotalChartProps, BigNumberVizProps } from '../types';
|
import { BigNumberTotalChartProps, BigNumberVizProps } from '../types';
|
||||||
import { getDateFormatter, getOriginalLabel, parseMetricValue } from '../utils';
|
import { getDateFormatter, parseMetricValue } from '../utils';
|
||||||
import { Refs } from '../../types';
|
import { Refs } from '../../types';
|
||||||
|
|
||||||
export default function transformProps(
|
export default function transformProps(
|
||||||
@@ -45,7 +45,6 @@ export default function transformProps(
|
|||||||
datasource: { currencyFormats = {}, columnFormats = {} },
|
datasource: { currencyFormats = {}, columnFormats = {} },
|
||||||
} = chartProps;
|
} = chartProps;
|
||||||
const {
|
const {
|
||||||
metricNameFontSize,
|
|
||||||
headerFontSize,
|
headerFontSize,
|
||||||
metric = 'value',
|
metric = 'value',
|
||||||
subtitle,
|
subtitle,
|
||||||
@@ -59,12 +58,9 @@ export default function transformProps(
|
|||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
} = formData;
|
} = formData;
|
||||||
const refs: Refs = {};
|
const refs: Refs = {};
|
||||||
const { data = [], coltypes = [] } = queriesData[0] || {};
|
const { data = [], coltypes = [] } = queriesData[0];
|
||||||
const granularity = extractTimegrain(rawFormData as QueryFormData);
|
const granularity = extractTimegrain(rawFormData as QueryFormData);
|
||||||
const metrics = chartProps.datasource?.metrics || [];
|
|
||||||
const originalLabel = getOriginalLabel(metric, metrics);
|
|
||||||
const metricName = getMetricLabel(metric);
|
const metricName = getMetricLabel(metric);
|
||||||
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
|
||||||
const formattedSubtitle = subtitle?.trim() ? subtitle : subheader || '';
|
const formattedSubtitle = subtitle?.trim() ? subtitle : subheader || '';
|
||||||
const formattedSubtitleFontSize = subtitle?.trim()
|
const formattedSubtitleFontSize = subtitle?.trim()
|
||||||
? (subtitleFontSize ?? 1)
|
? (subtitleFontSize ?? 1)
|
||||||
@@ -107,6 +103,7 @@ export default function transformProps(
|
|||||||
const colorThresholdFormatters =
|
const colorThresholdFormatters =
|
||||||
getColorFormatters(conditionalFormatting, data, false) ??
|
getColorFormatters(conditionalFormatting, data, false) ??
|
||||||
defaultColorFormatters;
|
defaultColorFormatters;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -119,8 +116,5 @@ export default function transformProps(
|
|||||||
onContextMenu,
|
onContextMenu,
|
||||||
refs,
|
refs,
|
||||||
colorThresholdFormatters,
|
colorThresholdFormatters,
|
||||||
metricName: originalLabel,
|
|
||||||
showMetricName,
|
|
||||||
metricNameFontSize,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { PureComponent, MouseEvent, createRef } from 'react';
|
import { PureComponent, MouseEvent } from 'react';
|
||||||
import {
|
import {
|
||||||
t,
|
t,
|
||||||
getNumberFormatter,
|
getNumberFormatter,
|
||||||
@@ -35,7 +35,6 @@ const defaultNumberFormatter = getNumberFormatter();
|
|||||||
|
|
||||||
const PROPORTION = {
|
const PROPORTION = {
|
||||||
// text size: proportion of the chart container sans trendline
|
// text size: proportion of the chart container sans trendline
|
||||||
METRIC_NAME: 0.125,
|
|
||||||
KICKER: 0.1,
|
KICKER: 0.1,
|
||||||
HEADER: 0.3,
|
HEADER: 0.3,
|
||||||
SUBHEADER: 0.125,
|
SUBHEADER: 0.125,
|
||||||
@@ -43,20 +42,13 @@ const PROPORTION = {
|
|||||||
TRENDLINE: 0.3,
|
TRENDLINE: 0.3,
|
||||||
};
|
};
|
||||||
|
|
||||||
type BigNumberVisState = {
|
class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||||
elementsRendered: boolean;
|
|
||||||
recalculateTrigger: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
className: '',
|
className: '',
|
||||||
headerFormatter: defaultNumberFormatter,
|
headerFormatter: defaultNumberFormatter,
|
||||||
formatTime: getTimeFormatter(SMART_DATE_VERBOSE_ID),
|
formatTime: getTimeFormatter(SMART_DATE_VERBOSE_ID),
|
||||||
headerFontSize: PROPORTION.HEADER,
|
headerFontSize: PROPORTION.HEADER,
|
||||||
kickerFontSize: PROPORTION.KICKER,
|
kickerFontSize: PROPORTION.KICKER,
|
||||||
metricNameFontSize: PROPORTION.METRIC_NAME,
|
|
||||||
showMetricName: true,
|
|
||||||
mainColor: BRAND_COLOR,
|
mainColor: BRAND_COLOR,
|
||||||
showTimestamp: false,
|
showTimestamp: false,
|
||||||
showTrendLine: false,
|
showTrendLine: false,
|
||||||
@@ -66,40 +58,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
|||||||
timeRangeFixed: false,
|
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() {
|
getClassName() {
|
||||||
const { className, showTrendLine, bigNumberFallback } = this.props;
|
const { className, showTrendLine, bigNumberFallback } = this.props;
|
||||||
const names = `superset-legacy-chart-big-number ${className} ${
|
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) {
|
renderKicker(maxHeight: number) {
|
||||||
const { timestamp, showTimestamp, formatTime, width } = this.props;
|
const { timestamp, showTimestamp, formatTime, width } = this.props;
|
||||||
if (
|
if (
|
||||||
@@ -191,7 +118,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={this.kickerRef}
|
|
||||||
className="kicker"
|
className="kicker"
|
||||||
style={{
|
style={{
|
||||||
fontSize,
|
fontSize,
|
||||||
@@ -247,7 +173,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={this.headerRef}
|
|
||||||
className="header-line"
|
className="header-line"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -286,7 +211,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={this.subheaderRef}
|
|
||||||
className="subheader-line"
|
className="subheader-line"
|
||||||
style={{
|
style={{
|
||||||
fontSize,
|
fontSize,
|
||||||
@@ -332,7 +256,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
ref={this.subtitleRef}
|
|
||||||
className="subtitle-line subheader-line"
|
className="subtitle-line subheader-line"
|
||||||
style={{
|
style={{
|
||||||
fontSize: `${fontSize}px`,
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
showTrendLine,
|
showTrendLine,
|
||||||
@@ -429,7 +323,6 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
|||||||
kickerFontSize,
|
kickerFontSize,
|
||||||
headerFontSize,
|
headerFontSize,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
metricNameFontSize,
|
|
||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const className = this.getClassName();
|
const className = this.getClassName();
|
||||||
@@ -437,31 +330,11 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
|||||||
if (showTrendLine) {
|
if (showTrendLine) {
|
||||||
const chartHeight = Math.floor(PROPORTION.TRENDLINE * height);
|
const chartHeight = Math.floor(PROPORTION.TRENDLINE * height);
|
||||||
const allTextHeight = height - chartHeight;
|
const allTextHeight = height - chartHeight;
|
||||||
const shouldApplyOverflow = this.shouldApplyOverflow(allTextHeight);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div
|
<div className="text-container" style={{ height: allTextHeight }}>
|
||||||
className="text-container"
|
|
||||||
style={{
|
|
||||||
height: allTextHeight,
|
|
||||||
...(shouldApplyOverflow
|
|
||||||
? {
|
|
||||||
display: 'block',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
overflowX: 'hidden',
|
|
||||||
overflowY: 'auto',
|
|
||||||
width: '100%',
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{this.renderFallbackWarning()}
|
{this.renderFallbackWarning()}
|
||||||
{this.renderMetricName(
|
|
||||||
Math.ceil(
|
|
||||||
(metricNameFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
{this.renderKicker(
|
{this.renderKicker(
|
||||||
Math.ceil(
|
Math.ceil(
|
||||||
(kickerFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
|
(kickerFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
|
||||||
@@ -483,33 +356,16 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const shouldApplyOverflow = this.shouldApplyOverflow(height);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={className} style={{ height }}>
|
||||||
className={className}
|
{this.renderFallbackWarning()}
|
||||||
style={{
|
{this.renderKicker((kickerFontSize || 0) * height)}
|
||||||
height,
|
{this.renderHeader(Math.ceil(headerFontSize * height))}
|
||||||
...(shouldApplyOverflow
|
{this.rendermetricComparisonSummary(
|
||||||
? {
|
Math.ceil(subheaderFontSize * height),
|
||||||
display: 'block',
|
)}
|
||||||
boxSizing: 'border-box',
|
{this.renderSubtitle(Math.ceil(subtitleFontSize * height))}
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -544,12 +400,7 @@ export default styled(BigNumberVis)`
|
|||||||
|
|
||||||
.kicker {
|
.kicker {
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
margin-bottom: ${theme.gridUnit * 2}px;
|
padding-bottom: 2em;
|
||||||
}
|
|
||||||
|
|
||||||
.metric-name {
|
|
||||||
line-height: 1em;
|
|
||||||
margin-bottom: ${theme.gridUnit * 2}px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-line {
|
.header-line {
|
||||||
@@ -565,12 +416,12 @@ export default styled(BigNumberVis)`
|
|||||||
|
|
||||||
.subheader-line {
|
.subheader-line {
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
margin-bottom: ${theme.gridUnit * 2}px;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle-line {
|
.subtitle-line {
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
margin-bottom: ${theme.gridUnit * 2}px;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-fallback-value {
|
&.is-fallback-value {
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ import {
|
|||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
subtitleControl,
|
subtitleControl,
|
||||||
showMetricNameControl,
|
|
||||||
metricNameFontSizeWithVisibility,
|
|
||||||
} from '../sharedControls';
|
} from '../sharedControls';
|
||||||
|
|
||||||
const config: ControlPanelConfig = {
|
const config: ControlPanelConfig = {
|
||||||
@@ -143,8 +141,6 @@ const config: ControlPanelConfig = {
|
|||||||
[subheaderFontSize],
|
[subheaderFontSize],
|
||||||
[subtitleControl],
|
[subtitleControl],
|
||||||
[subtitleFontSize],
|
[subtitleFontSize],
|
||||||
[showMetricNameControl],
|
|
||||||
[metricNameFontSizeWithVisibility],
|
|
||||||
['y_axis_format'],
|
['y_axis_format'],
|
||||||
['currency_format'],
|
['currency_format'],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ const metadata = {
|
|||||||
name: t('Big Number with Trendline'),
|
name: t('Big Number with Trendline'),
|
||||||
tags: [
|
tags: [
|
||||||
t('Advanced-Analytics'),
|
t('Advanced-Analytics'),
|
||||||
t('ECharts'),
|
|
||||||
t('Line'),
|
t('Line'),
|
||||||
t('Percentages'),
|
t('Percentages'),
|
||||||
t('Featured'),
|
t('Featured'),
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ jest.mock('@superset-ui/core', () => ({
|
|||||||
jest.mock('../utils', () => ({
|
jest.mock('../utils', () => ({
|
||||||
getDateFormatter: jest.fn(() => (v: any) => `${v}pm`),
|
getDateFormatter: jest.fn(() => (v: any) => `${v}pm`),
|
||||||
parseMetricValue: jest.fn(val => Number(val)),
|
parseMetricValue: jest.fn(val => Number(val)),
|
||||||
getOriginalLabel: jest.fn((metric, metrics) => metric),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../../utils/tooltip', () => ({
|
jest.mock('../../utils/tooltip', () => ({
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
BigNumberWithTrendlineChartProps,
|
BigNumberWithTrendlineChartProps,
|
||||||
TimeSeriesDatum,
|
TimeSeriesDatum,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getDateFormatter, parseMetricValue, getOriginalLabel } from '../utils';
|
import { getDateFormatter, parseMetricValue } from '../utils';
|
||||||
import { getDefaultTooltip } from '../../utils/tooltip';
|
import { getDefaultTooltip } from '../../utils/tooltip';
|
||||||
import { Refs } from '../../types';
|
import { Refs } from '../../types';
|
||||||
|
|
||||||
@@ -62,7 +62,6 @@ export default function transformProps(
|
|||||||
compareLag: compareLag_,
|
compareLag: compareLag_,
|
||||||
compareSuffix = '',
|
compareSuffix = '',
|
||||||
timeFormat,
|
timeFormat,
|
||||||
metricNameFontSize,
|
|
||||||
headerFontSize,
|
headerFontSize,
|
||||||
metric = 'value',
|
metric = 'value',
|
||||||
showTimestamp,
|
showTimestamp,
|
||||||
@@ -97,9 +96,6 @@ export default function transformProps(
|
|||||||
const aggregatedData = hasAggregatedData ? aggregatedQueryData.data[0] : null;
|
const aggregatedData = hasAggregatedData ? aggregatedQueryData.data[0] : null;
|
||||||
const refs: Refs = {};
|
const refs: Refs = {};
|
||||||
const metricName = getMetricLabel(metric);
|
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;
|
const compareLag = Number(compareLag_) || 0;
|
||||||
let formattedSubheader = subheader;
|
let formattedSubheader = subheader;
|
||||||
|
|
||||||
@@ -307,9 +303,6 @@ export default function transformProps(
|
|||||||
headerFormatter,
|
headerFormatter,
|
||||||
formatTime,
|
formatTime,
|
||||||
formData,
|
formData,
|
||||||
metricName: originalLabel,
|
|
||||||
showMetricName,
|
|
||||||
metricNameFontSize,
|
|
||||||
headerFontSize,
|
headerFontSize,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
|||||||
@@ -21,68 +21,106 @@
|
|||||||
import { t } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
import { CustomControlItem } from '@superset-ui/chart-controls';
|
import { CustomControlItem } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
const FONT_SIZE_OPTIONS_SMALL = [
|
export const headerFontSize: CustomControlItem = {
|
||||||
{ label: t('Tiny'), value: 0.125 },
|
name: 'header_font_size',
|
||||||
{ label: t('Small'), value: 0.15 },
|
config: {
|
||||||
{ label: t('Normal'), value: 0.2 },
|
type: 'SelectControl',
|
||||||
{ label: t('Large'), value: 0.3 },
|
label: t('Big Number Font Size'),
|
||||||
{ label: t('Huge'), value: 0.4 },
|
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 = [
|
export const subtitleFontSize: CustomControlItem = {
|
||||||
{ label: t('Tiny'), value: 0.2 },
|
name: 'subtitle_font_size',
|
||||||
{ label: t('Small'), value: 0.3 },
|
config: {
|
||||||
{ label: t('Normal'), value: 0.4 },
|
type: 'SelectControl',
|
||||||
{ label: t('Large'), value: 0.5 },
|
label: t('Subtitle Font Size'),
|
||||||
{ label: t('Huge'), value: 0.6 },
|
renderTrigger: true,
|
||||||
];
|
clearable: false,
|
||||||
|
default: 0.15,
|
||||||
function makeFontSizeControl(
|
// Values represent the percentage of space a subtitle should take
|
||||||
name: string,
|
options: [
|
||||||
label: string,
|
{
|
||||||
defaultValue: number,
|
label: t('Tiny'),
|
||||||
options: { label: string; value: number }[],
|
value: 0.125,
|
||||||
): CustomControlItem {
|
},
|
||||||
return {
|
{
|
||||||
name,
|
label: t('Small'),
|
||||||
config: {
|
value: 0.15,
|
||||||
type: 'SelectControl',
|
},
|
||||||
label: t(label),
|
{
|
||||||
renderTrigger: true,
|
label: t('Normal'),
|
||||||
clearable: false,
|
value: 0.2,
|
||||||
default: defaultValue,
|
},
|
||||||
options,
|
{
|
||||||
},
|
label: t('Large'),
|
||||||
};
|
value: 0.3,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
export const headerFontSize = makeFontSizeControl(
|
label: t('Huge'),
|
||||||
'header_font_size',
|
value: 0.4,
|
||||||
'Big Number Font Size',
|
},
|
||||||
0.4,
|
],
|
||||||
FONT_SIZE_OPTIONS_LARGE,
|
},
|
||||||
);
|
};
|
||||||
|
export const subheaderFontSize: CustomControlItem = {
|
||||||
export const subtitleFontSize = makeFontSizeControl(
|
name: 'subheader_font_size',
|
||||||
'subtitle_font_size',
|
config: {
|
||||||
'Subtitle Font Size',
|
type: 'SelectControl',
|
||||||
0.15,
|
label: t('Subheader Font Size'),
|
||||||
FONT_SIZE_OPTIONS_SMALL,
|
renderTrigger: true,
|
||||||
);
|
clearable: false,
|
||||||
|
default: 0.15,
|
||||||
export const subheaderFontSize = makeFontSizeControl(
|
// Values represent the percentage of space a subheader should take
|
||||||
'subheader_font_size',
|
options: [
|
||||||
'Subheader Font Size',
|
{
|
||||||
0.15,
|
label: t('Tiny'),
|
||||||
FONT_SIZE_OPTIONS_SMALL,
|
value: 0.125,
|
||||||
);
|
},
|
||||||
|
{
|
||||||
export const metricNameFontSize = makeFontSizeControl(
|
label: t('Small'),
|
||||||
'metric_name_font_size',
|
value: 0.15,
|
||||||
'Metric Name Font Size',
|
},
|
||||||
0.15,
|
{
|
||||||
FONT_SIZE_OPTIONS_SMALL,
|
label: t('Normal'),
|
||||||
);
|
value: 0.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Large'),
|
||||||
|
value: 0.3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Huge'),
|
||||||
|
value: 0.4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const subtitleControl: CustomControlItem = {
|
export const subtitleControl: CustomControlItem = {
|
||||||
name: 'subtitle',
|
name: 'subtitle',
|
||||||
@@ -93,23 +131,3 @@ export const subtitleControl: CustomControlItem = {
|
|||||||
description: t('Description text that shows up below your Big Number'),
|
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;
|
bigNumberFallback?: TimeSeriesDatum;
|
||||||
headerFormatter: ValueFormatter | TimeFormatter;
|
headerFormatter: ValueFormatter | TimeFormatter;
|
||||||
formatTime?: TimeFormatter;
|
formatTime?: TimeFormatter;
|
||||||
metricName?: string;
|
|
||||||
friendlyMetricName?: string;
|
|
||||||
metricNameFontSize?: number;
|
|
||||||
showMetricName?: boolean;
|
|
||||||
headerFontSize: number;
|
headerFontSize: number;
|
||||||
kickerFontSize?: number;
|
kickerFontSize?: number;
|
||||||
subheader?: string;
|
subheader?: string;
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ import utc from 'dayjs/plugin/utc';
|
|||||||
import {
|
import {
|
||||||
getTimeFormatter,
|
getTimeFormatter,
|
||||||
getTimeFormatterForGranularity,
|
getTimeFormatterForGranularity,
|
||||||
isAdhocMetricSimple,
|
|
||||||
isSavedMetric,
|
|
||||||
Metric,
|
|
||||||
QueryFormMetric,
|
|
||||||
SMART_DATE_ID,
|
SMART_DATE_ID,
|
||||||
TimeGranularity,
|
TimeGranularity,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
@@ -51,43 +47,3 @@ export const getDateFormatter = (
|
|||||||
timeFormat === SMART_DATE_ID
|
timeFormat === SMART_DATE_ID
|
||||||
? getTimeFormatterForGranularity(granularity)
|
? getTimeFormatterForGranularity(granularity)
|
||||||
: getTimeFormatter(timeFormat ?? fallbackFormat);
|
: 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 }],
|
exampleGallery: [{ url: example1 }, { url: example2 }],
|
||||||
name: t('Sankey Chart'),
|
name: t('Sankey Chart'),
|
||||||
tags: [t('Directional'), t('ECharts'), t('Distribution'), t('Flow')],
|
tags: [t('Directional'), t('Distribution'), t('Flow')],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
}),
|
}),
|
||||||
transformProps,
|
transformProps,
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export default class EchartsTimeseriesChartPlugin extends EchartsChartPlugin<
|
|||||||
name: t('Generic Chart'),
|
name: t('Generic Chart'),
|
||||||
tags: [
|
tags: [
|
||||||
t('Advanced-Analytics'),
|
t('Advanced-Analytics'),
|
||||||
t('ECharts'),
|
|
||||||
t('Line'),
|
t('Line'),
|
||||||
t('Predictive'),
|
t('Predictive'),
|
||||||
t('Time'),
|
t('Time'),
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
/* eslint-disable import/no-extraneous-dependencies */
|
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -25,7 +24,6 @@ import {
|
|||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
DragEvent,
|
DragEvent,
|
||||||
useEffect,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -41,9 +39,8 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
} from 'react-table';
|
} from 'react-table';
|
||||||
import { matchSorter, rankings } from 'match-sorter';
|
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 { isEqual } from 'lodash';
|
||||||
import { Space } from 'antd';
|
|
||||||
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
|
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
|
||||||
import SelectPageSize, {
|
import SelectPageSize, {
|
||||||
SelectPageSizeProps,
|
SelectPageSizeProps,
|
||||||
@@ -53,8 +50,6 @@ import SimplePagination from './components/Pagination';
|
|||||||
import useSticky from './hooks/useSticky';
|
import useSticky from './hooks/useSticky';
|
||||||
import { PAGE_SIZE_OPTIONS } from '../consts';
|
import { PAGE_SIZE_OPTIONS } from '../consts';
|
||||||
import { sortAlphanumericCaseInsensitive } from './utils/sortAlphanumericCaseInsensitive';
|
import { sortAlphanumericCaseInsensitive } from './utils/sortAlphanumericCaseInsensitive';
|
||||||
import { SearchOption, SortByItem } from '../types';
|
|
||||||
import SearchSelectDropdown from './components/SearchSelectDropdown';
|
|
||||||
|
|
||||||
export interface DataTableProps<D extends object> extends TableOptions<D> {
|
export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||||
tableClassName?: string;
|
tableClassName?: string;
|
||||||
@@ -67,12 +62,7 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
|||||||
height?: string | number;
|
height?: string | number;
|
||||||
serverPagination?: boolean;
|
serverPagination?: boolean;
|
||||||
onServerPaginationChange: (pageNumber: number, pageSize: number) => void;
|
onServerPaginationChange: (pageNumber: number, pageSize: number) => void;
|
||||||
serverPaginationData: {
|
serverPaginationData: { pageSize?: number; currentPage?: number };
|
||||||
pageSize?: number;
|
|
||||||
currentPage?: number;
|
|
||||||
sortBy?: SortByItem[];
|
|
||||||
searchColumn?: string;
|
|
||||||
};
|
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
noResults?: string | ((filterString: string) => ReactNode);
|
noResults?: string | ((filterString: string) => ReactNode);
|
||||||
sticky?: boolean;
|
sticky?: boolean;
|
||||||
@@ -81,14 +71,6 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
|||||||
onColumnOrderChange: () => void;
|
onColumnOrderChange: () => void;
|
||||||
renderGroupingHeaders?: () => JSX.Element;
|
renderGroupingHeaders?: () => JSX.Element;
|
||||||
renderTimeComparisonDropdown?: () => 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> {
|
export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
|
||||||
@@ -99,20 +81,6 @@ const sortTypes = {
|
|||||||
alphanumeric: sortAlphanumericCaseInsensitive,
|
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
|
// Be sure to pass our updateMyData and the skipReset option
|
||||||
export default typedMemo(function DataTable<D extends object>({
|
export default typedMemo(function DataTable<D extends object>({
|
||||||
tableClassName,
|
tableClassName,
|
||||||
@@ -137,14 +105,6 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
onColumnOrderChange,
|
onColumnOrderChange,
|
||||||
renderGroupingHeaders,
|
renderGroupingHeaders,
|
||||||
renderTimeComparisonDropdown,
|
renderTimeComparisonDropdown,
|
||||||
handleSortByChange,
|
|
||||||
sortByFromParent = [],
|
|
||||||
manualSearch = false,
|
|
||||||
onSearchChange,
|
|
||||||
initialSearchText,
|
|
||||||
searchInputId,
|
|
||||||
onSearchColChange,
|
|
||||||
searchOptions,
|
|
||||||
...moreUseTableOptions
|
...moreUseTableOptions
|
||||||
}: DataTableProps<D>): JSX.Element {
|
}: DataTableProps<D>): JSX.Element {
|
||||||
const tableHooks: PluginHook<D>[] = [
|
const tableHooks: PluginHook<D>[] = [
|
||||||
@@ -155,7 +115,6 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
doSticky ? useSticky : [],
|
doSticky ? useSticky : [],
|
||||||
hooks || [],
|
hooks || [],
|
||||||
].flat();
|
].flat();
|
||||||
|
|
||||||
const columnNames = Object.keys(data?.[0] || {});
|
const columnNames = Object.keys(data?.[0] || {});
|
||||||
const previousColumnNames = usePrevious(columnNames);
|
const previousColumnNames = usePrevious(columnNames);
|
||||||
const resultsSize = serverPagination ? rowCount : data.length;
|
const resultsSize = serverPagination ? rowCount : data.length;
|
||||||
@@ -168,8 +127,7 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
...initialState_,
|
...initialState_,
|
||||||
// zero length means all pages, the `usePagination` plugin does not
|
// zero length means all pages, the `usePagination` plugin does not
|
||||||
// understand pageSize = 0
|
// understand pageSize = 0
|
||||||
// sortBy: sortByRef.current,
|
sortBy: sortByRef.current,
|
||||||
sortBy: serverPagination ? sortByFromParent : sortByRef.current,
|
|
||||||
pageSize: initialPageSize > 0 ? initialPageSize : resultsSize || 10,
|
pageSize: initialPageSize > 0 ? initialPageSize : resultsSize || 10,
|
||||||
};
|
};
|
||||||
const defaultWrapperRef = useRef<HTMLDivElement>(null);
|
const defaultWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -230,13 +188,7 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
wrapStickyTable,
|
wrapStickyTable,
|
||||||
setColumnOrder,
|
setColumnOrder,
|
||||||
allColumns,
|
allColumns,
|
||||||
state: {
|
state: { pageIndex, pageSize, globalFilter: filterValue, sticky = {} },
|
||||||
pageIndex,
|
|
||||||
pageSize,
|
|
||||||
globalFilter: filterValue,
|
|
||||||
sticky = {},
|
|
||||||
sortBy,
|
|
||||||
},
|
|
||||||
} = useTable<D>(
|
} = useTable<D>(
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
@@ -246,46 +198,10 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
globalFilter: defaultGlobalFilter,
|
globalFilter: defaultGlobalFilter,
|
||||||
sortTypes,
|
sortTypes,
|
||||||
autoResetSortBy: !isEqual(columnNames, previousColumnNames),
|
autoResetSortBy: !isEqual(columnNames, previousColumnNames),
|
||||||
manualSortBy: !!serverPagination,
|
|
||||||
...moreUseTableOptions,
|
...moreUseTableOptions,
|
||||||
},
|
},
|
||||||
...tableHooks,
|
...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
|
// make setPageSize accept 0
|
||||||
const setPageSize = (size: number) => {
|
const setPageSize = (size: number) => {
|
||||||
if (serverPagination) {
|
if (serverPagination) {
|
||||||
@@ -439,7 +355,6 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
resultOnPageChange = (pageNumber: number) =>
|
resultOnPageChange = (pageNumber: number) =>
|
||||||
onServerPaginationChange(pageNumber, serverPageSize);
|
onServerPaginationChange(pageNumber, serverPageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
@@ -466,31 +381,16 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{searchInput ? (
|
{searchInput ? (
|
||||||
<StyledSpace className="col-sm-6">
|
<div 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>
|
|
||||||
)}
|
|
||||||
<GlobalFilter<D>
|
<GlobalFilter<D>
|
||||||
searchInput={
|
searchInput={
|
||||||
typeof searchInput === 'boolean' ? undefined : searchInput
|
typeof searchInput === 'boolean' ? undefined : searchInput
|
||||||
}
|
}
|
||||||
preGlobalFilteredRows={preGlobalFilteredRows}
|
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||||
setGlobalFilter={
|
setGlobalFilter={setGlobalFilter}
|
||||||
manualSearch ? handleSearchChange : setGlobalFilter
|
filterValue={filterValue}
|
||||||
}
|
|
||||||
filterValue={manualSearch ? initialSearchText : filterValue}
|
|
||||||
id={searchInputId}
|
|
||||||
serverPagination={!!serverPagination}
|
|
||||||
rowCount={rowCount}
|
|
||||||
/>
|
/>
|
||||||
</StyledSpace>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{renderTimeComparisonDropdown ? (
|
{renderTimeComparisonDropdown ? (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -16,13 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import {
|
import { memo, ComponentType, ChangeEventHandler } from 'react';
|
||||||
memo,
|
|
||||||
ComponentType,
|
|
||||||
ChangeEventHandler,
|
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
} from 'react';
|
|
||||||
import { Row, FilterValue } from 'react-table';
|
import { Row, FilterValue } from 'react-table';
|
||||||
import useAsyncState from '../utils/useAsyncState';
|
import useAsyncState from '../utils/useAsyncState';
|
||||||
|
|
||||||
@@ -30,12 +24,8 @@ export interface SearchInputProps {
|
|||||||
count: number;
|
count: number;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||||
onBlur?: () => void;
|
|
||||||
inputRef?: React.RefObject<HTMLInputElement>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSearchFocused = new Map();
|
|
||||||
|
|
||||||
export interface GlobalFilterProps<D extends object> {
|
export interface GlobalFilterProps<D extends object> {
|
||||||
preGlobalFilteredRows: Row<D>[];
|
preGlobalFilteredRows: Row<D>[];
|
||||||
// filter value cannot be `undefined` otherwise React will report component
|
// filter value cannot be `undefined` otherwise React will report component
|
||||||
@@ -43,28 +33,17 @@ export interface GlobalFilterProps<D extends object> {
|
|||||||
filterValue: string;
|
filterValue: string;
|
||||||
setGlobalFilter: (filterValue: FilterValue) => void;
|
setGlobalFilter: (filterValue: FilterValue) => void;
|
||||||
searchInput?: ComponentType<SearchInputProps>;
|
searchInput?: ComponentType<SearchInputProps>;
|
||||||
id?: string;
|
|
||||||
serverPagination: boolean;
|
|
||||||
rowCount: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DefaultSearchInput({
|
function DefaultSearchInput({ count, value, onChange }: SearchInputProps) {
|
||||||
count,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onBlur,
|
|
||||||
inputRef,
|
|
||||||
}: SearchInputProps) {
|
|
||||||
return (
|
return (
|
||||||
<span className="dt-global-filter">
|
<span className="dt-global-filter">
|
||||||
Search{' '}
|
Search{' '}
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
|
||||||
className="form-control input-sm"
|
className="form-control input-sm"
|
||||||
placeholder={`${count} records...`}
|
placeholder={`${count} records...`}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -77,13 +56,8 @@ export default (memo as <T>(fn: T) => T)(function GlobalFilter<
|
|||||||
filterValue = '',
|
filterValue = '',
|
||||||
searchInput,
|
searchInput,
|
||||||
setGlobalFilter,
|
setGlobalFilter,
|
||||||
id = '',
|
|
||||||
serverPagination,
|
|
||||||
rowCount,
|
|
||||||
}: GlobalFilterProps<D>) {
|
}: GlobalFilterProps<D>) {
|
||||||
const count = serverPagination ? rowCount : preGlobalFilteredRows.length;
|
const count = preGlobalFilteredRows.length;
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const [value, setValue] = useAsyncState(
|
const [value, setValue] = useAsyncState(
|
||||||
filterValue,
|
filterValue,
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
@@ -92,37 +66,17 @@ export default (memo as <T>(fn: T) => T)(function GlobalFilter<
|
|||||||
200,
|
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;
|
const SearchInput = searchInput || DefaultSearchInput;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchInput
|
<SearchInput
|
||||||
count={count}
|
count={count}
|
||||||
value={value}
|
value={value}
|
||||||
inputRef={inputRef}
|
onChange={e => {
|
||||||
onChange={handleChange}
|
const target = e.target as HTMLInputElement;
|
||||||
onBlur={handleBlur}
|
e.preventDefault();
|
||||||
|
setValue(target.value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -115,11 +115,3 @@ declare module 'react-table' {
|
|||||||
extends UseTableHooks<D>,
|
extends UseTableHooks<D>,
|
||||||
UseSortByHooks<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 { SetDataMaskHook } from '@superset-ui/core';
|
||||||
import { TableOwnState } from '../types/react-table';
|
|
||||||
|
|
||||||
export const updateExternalFormData = (
|
export const updateExternalFormData = (
|
||||||
setDataMask: SetDataMaskHook = () => {},
|
setDataMask: SetDataMaskHook = () => {},
|
||||||
@@ -31,11 +30,3 @@ export const updateExternalFormData = (
|
|||||||
pageSize,
|
pageSize,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateTableOwnState = (
|
|
||||||
setDataMask: SetDataMaskHook = () => {},
|
|
||||||
modifiedOwnState: TableOwnState,
|
|
||||||
) =>
|
|
||||||
setDataMask({
|
|
||||||
ownState: modifiedOwnState,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
KeyboardEvent as ReactKeyboardEvent,
|
KeyboardEvent as ReactKeyboardEvent,
|
||||||
useEffect,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -62,12 +61,10 @@ import {
|
|||||||
PlusCircleOutlined,
|
PlusCircleOutlined,
|
||||||
TableOutlined,
|
TableOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { debounce, isEmpty, isEqual } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import {
|
import {
|
||||||
ColorSchemeEnum,
|
ColorSchemeEnum,
|
||||||
DataColumnMeta,
|
DataColumnMeta,
|
||||||
SearchOption,
|
|
||||||
SortByItem,
|
|
||||||
TableChartTransformedProps,
|
TableChartTransformedProps,
|
||||||
} from './types';
|
} from './types';
|
||||||
import DataTable, {
|
import DataTable, {
|
||||||
@@ -80,7 +77,7 @@ import DataTable, {
|
|||||||
import Styles from './Styles';
|
import Styles from './Styles';
|
||||||
import { formatColumnValue } from './utils/formatValue';
|
import { formatColumnValue } from './utils/formatValue';
|
||||||
import { PAGE_SIZE_OPTIONS } from './consts';
|
import { PAGE_SIZE_OPTIONS } from './consts';
|
||||||
import { updateTableOwnState } from './DataTable/utils/externalAPIs';
|
import { updateExternalFormData } from './DataTable/utils/externalAPIs';
|
||||||
import getScrollBarSize from './DataTable/utils/getScrollBarSize';
|
import getScrollBarSize from './DataTable/utils/getScrollBarSize';
|
||||||
|
|
||||||
type ValueRange = [number, number];
|
type ValueRange = [number, number];
|
||||||
@@ -179,26 +176,20 @@ function SortIcon<D extends object>({ column }: { column: ColumnInstance<D> }) {
|
|||||||
return sortIcon;
|
return sortIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchInput = ({
|
function SearchInput({ count, value, onChange }: SearchInputProps) {
|
||||||
count,
|
return (
|
||||||
value,
|
<span className="dt-global-filter">
|
||||||
onChange,
|
{t('Search')}{' '}
|
||||||
onBlur,
|
<input
|
||||||
inputRef,
|
aria-label={t('Search %s records', count)}
|
||||||
}: SearchInputProps) => (
|
className="form-control input-sm"
|
||||||
<span className="dt-global-filter">
|
placeholder={tn('search.num_records', count)}
|
||||||
{t('Search')}{' '}
|
value={value}
|
||||||
<input
|
onChange={onChange}
|
||||||
ref={inputRef}
|
/>
|
||||||
aria-label={t('Search %s records', count)}
|
</span>
|
||||||
className="form-control input-sm"
|
);
|
||||||
placeholder={tn('search.num_records', count)}
|
}
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
function SelectPageSize({
|
function SelectPageSize({
|
||||||
options,
|
options,
|
||||||
@@ -276,9 +267,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||||||
isUsingTimeComparison,
|
isUsingTimeComparison,
|
||||||
basicColorFormatters,
|
basicColorFormatters,
|
||||||
basicColorColumnFormatters,
|
basicColorColumnFormatters,
|
||||||
hasServerPageLengthChanged,
|
|
||||||
serverPageLength,
|
|
||||||
slice_id,
|
|
||||||
} = props;
|
} = props;
|
||||||
const comparisonColumns = [
|
const comparisonColumns = [
|
||||||
{ key: 'all', label: t('Display all') },
|
{ key: 'all', label: t('Display all') },
|
||||||
@@ -691,12 +679,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getColumnConfigs = useCallback(
|
const getColumnConfigs = useCallback(
|
||||||
(
|
(column: DataColumnMeta, i: number): ColumnWithLooseAccessor<D> => {
|
||||||
column: DataColumnMeta,
|
|
||||||
i: number,
|
|
||||||
): ColumnWithLooseAccessor<D> & {
|
|
||||||
columnKey: string;
|
|
||||||
} => {
|
|
||||||
const {
|
const {
|
||||||
key,
|
key,
|
||||||
label: originalLabel,
|
label: originalLabel,
|
||||||
@@ -783,7 +766,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||||||
// must use custom accessor to allow `.` in column names
|
// must use custom accessor to allow `.` in column names
|
||||||
// typing is incorrect in current version of `@types/react-table`
|
// typing is incorrect in current version of `@types/react-table`
|
||||||
// so we ask TS not to check.
|
// so we ask TS not to check.
|
||||||
columnKey: key,
|
|
||||||
accessor: ((datum: D) => datum[key]) as never,
|
accessor: ((datum: D) => datum[key]) as never,
|
||||||
Cell: ({ value, row }: { value: DataRecordValue; row: Row<D> }) => {
|
Cell: ({ value, row }: { value: DataRecordValue; row: Row<D> }) => {
|
||||||
const [isHtml, text] = formatColumnValue(column, value);
|
const [isHtml, text] = formatColumnValue(column, value);
|
||||||
@@ -1076,50 +1058,13 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||||||
[visibleColumnsMeta, getColumnConfigs],
|
[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(
|
const handleServerPaginationChange = useCallback(
|
||||||
(pageNumber: number, pageSize: number) => {
|
(pageNumber: number, pageSize: number) => {
|
||||||
const modifiedOwnState = {
|
updateExternalFormData(setDataMask, pageNumber, pageSize);
|
||||||
...serverPaginationData,
|
|
||||||
currentPage: pageNumber,
|
|
||||||
pageSize,
|
|
||||||
};
|
|
||||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
|
||||||
},
|
},
|
||||||
[setDataMask],
|
[setDataMask],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasServerPageLengthChanged) {
|
|
||||||
const modifiedOwnState = {
|
|
||||||
...serverPaginationData,
|
|
||||||
currentPage: 0,
|
|
||||||
pageSize: serverPageLength,
|
|
||||||
};
|
|
||||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSizeChange = useCallback(
|
const handleSizeChange = useCallback(
|
||||||
({ width, height }: { width: number; height: number }) => {
|
({ width, height }: { width: number; height: number }) => {
|
||||||
setTableSize({ width, height });
|
setTableSize({ width, height });
|
||||||
@@ -1155,42 +1100,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||||||
|
|
||||||
const { width: widthFromState, height: heightFromState } = tableSize;
|
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 (
|
return (
|
||||||
<Styles>
|
<Styles>
|
||||||
<DataTable<D>
|
<DataTable<D>
|
||||||
@@ -1206,9 +1115,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||||||
serverPagination={serverPagination}
|
serverPagination={serverPagination}
|
||||||
onServerPaginationChange={handleServerPaginationChange}
|
onServerPaginationChange={handleServerPaginationChange}
|
||||||
onColumnOrderChange={() => setColumnOrderToggle(!columnOrderToggle)}
|
onColumnOrderChange={() => setColumnOrderToggle(!columnOrderToggle)}
|
||||||
initialSearchText={serverPaginationData?.searchText || ''}
|
|
||||||
sortByFromParent={serverPaginationData?.sortBy || []}
|
|
||||||
searchInputId={`${slice_id}-search`}
|
|
||||||
// 9 page items in > 340px works well even for 100+ pages
|
// 9 page items in > 340px works well even for 100+ pages
|
||||||
maxPageItemCount={width > 340 ? 9 : 7}
|
maxPageItemCount={width > 340 ? 9 : 7}
|
||||||
noResults={getNoResultsMessage}
|
noResults={getNoResultsMessage}
|
||||||
@@ -1222,11 +1128,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||||||
renderTimeComparisonDropdown={
|
renderTimeComparisonDropdown={
|
||||||
isUsingTimeComparison ? renderTimeComparisonDropdown : undefined
|
isUsingTimeComparison ? renderTimeComparisonDropdown : undefined
|
||||||
}
|
}
|
||||||
handleSortByChange={handleSortByChange}
|
|
||||||
onSearchColChange={handleChangeSearchCol}
|
|
||||||
manualSearch={serverPagination}
|
|
||||||
onSearchChange={debouncedSearch}
|
|
||||||
searchOptions={searchOptions}
|
|
||||||
/>
|
/>
|
||||||
</Styles>
|
</Styles>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
getMetricLabel,
|
getMetricLabel,
|
||||||
isPhysicalColumn,
|
isPhysicalColumn,
|
||||||
QueryFormOrderBy,
|
|
||||||
QueryMode,
|
QueryMode,
|
||||||
QueryObject,
|
QueryObject,
|
||||||
removeDuplicates,
|
removeDuplicates,
|
||||||
@@ -35,7 +34,7 @@ import {
|
|||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { TableChartFormData } from './types';
|
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,
|
* 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 moreProps: Partial<QueryObject> = {};
|
||||||
const ownState = options?.ownState ?? {};
|
const ownState = options?.ownState ?? {};
|
||||||
// Build Query flag to check if its for either download as csv, excel or json
|
if (formDataCopy.server_pagination) {
|
||||||
const isDownloadQuery =
|
moreProps.row_limit =
|
||||||
['csv', 'xlsx'].includes(formData?.result_format || '') ||
|
ownState.pageSize ?? formDataCopy.server_page_length;
|
||||||
(formData?.result_format === 'json' &&
|
moreProps.row_offset =
|
||||||
formData?.result_type === 'results');
|
(ownState.currentPage ?? 0) * (ownState.pageSize ?? 0);
|
||||||
|
|
||||||
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]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let queryObject = {
|
let queryObject = {
|
||||||
...baseQueryObject,
|
...baseQueryObject,
|
||||||
columns,
|
columns,
|
||||||
extras,
|
extras,
|
||||||
orderby:
|
orderby,
|
||||||
formData.server_pagination && sortByFromOwnState
|
|
||||||
? sortByFromOwnState
|
|
||||||
: orderby,
|
|
||||||
metrics,
|
metrics,
|
||||||
post_processing: postProcessing,
|
post_processing: postProcessing,
|
||||||
time_offsets: timeOffsets,
|
time_offsets: timeOffsets,
|
||||||
@@ -239,12 +216,11 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
|||||||
JSON.stringify(queryObject.filters)
|
JSON.stringify(queryObject.filters)
|
||||||
) {
|
) {
|
||||||
queryObject = { ...queryObject, row_offset: 0 };
|
queryObject = { ...queryObject, row_offset: 0 };
|
||||||
const modifiedOwnState = {
|
updateExternalFormData(
|
||||||
...(options?.ownState || {}),
|
options?.hooks?.setDataMask,
|
||||||
currentPage: 0,
|
0,
|
||||||
pageSize: queryObject.row_limit ?? 0,
|
queryObject.row_limit ?? 0,
|
||||||
};
|
);
|
||||||
updateTableOwnState(options?.hooks?.setDataMask, modifiedOwnState);
|
|
||||||
}
|
}
|
||||||
// Because we use same buildQuery for all table on the page we need split them by id
|
// Because we use same buildQuery for all table on the page we need split them by id
|
||||||
options?.hooks?.setCachedChanges({
|
options?.hooks?.setCachedChanges({
|
||||||
@@ -276,32 +252,12 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (formData.server_pagination) {
|
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 [
|
return [
|
||||||
{ ...queryObject },
|
{ ...queryObject },
|
||||||
{
|
{
|
||||||
...queryObject,
|
...queryObject,
|
||||||
time_offsets: [],
|
time_offsets: [],
|
||||||
row_limit: Number(formData?.row_limit) ?? 0,
|
row_limit: 0,
|
||||||
row_offset: 0,
|
row_offset: 0,
|
||||||
post_processing: [],
|
post_processing: [],
|
||||||
is_rowcount: true,
|
is_rowcount: true,
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ import {
|
|||||||
ControlStateMapping,
|
ControlStateMapping,
|
||||||
D3_TIME_FORMAT_OPTIONS,
|
D3_TIME_FORMAT_OPTIONS,
|
||||||
Dataset,
|
Dataset,
|
||||||
DEFAULT_MAX_ROW,
|
|
||||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
|
||||||
defineSavedMetrics,
|
defineSavedMetrics,
|
||||||
formatSelectOptions,
|
|
||||||
getStandardizedControls,
|
getStandardizedControls,
|
||||||
QueryModeLabel,
|
QueryModeLabel,
|
||||||
sections,
|
sections,
|
||||||
@@ -43,14 +40,11 @@ import {
|
|||||||
getMetricLabel,
|
getMetricLabel,
|
||||||
isAdhocColumn,
|
isAdhocColumn,
|
||||||
isPhysicalColumn,
|
isPhysicalColumn,
|
||||||
legacyValidateInteger,
|
|
||||||
QueryFormColumn,
|
QueryFormColumn,
|
||||||
QueryFormMetric,
|
QueryFormMetric,
|
||||||
QueryMode,
|
QueryMode,
|
||||||
SMART_DATE_ID,
|
SMART_DATE_ID,
|
||||||
t,
|
t,
|
||||||
validateMaxValue,
|
|
||||||
validateServerPagination,
|
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
|
|
||||||
import { isEmpty, last } from 'lodash';
|
import { isEmpty, last } from 'lodash';
|
||||||
@@ -194,15 +188,6 @@ const processComparisonColumns = (columns: any[], suffix: string) =>
|
|||||||
})
|
})
|
||||||
.flat();
|
.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 = {
|
const config: ControlPanelConfig = {
|
||||||
controlPanelSections: [
|
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',
|
name: 'server_page_length',
|
||||||
config: {
|
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',
|
name: 'order_desc',
|
||||||
|
|||||||
@@ -90,15 +90,6 @@ const processDataRecords = memoizeOne(function processDataRecords(
|
|||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a map to store cached values per slice
|
|
||||||
const sliceCache = new Map<
|
|
||||||
number,
|
|
||||||
{
|
|
||||||
cachedServerLength: number;
|
|
||||||
passedColumns?: DataColumnMeta[];
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
const calculateDifferences = (
|
const calculateDifferences = (
|
||||||
originalValue: number,
|
originalValue: number,
|
||||||
comparisonValue: number,
|
comparisonValue: number,
|
||||||
@@ -489,7 +480,6 @@ const transformProps = (
|
|||||||
comparison_color_enabled: comparisonColorEnabled = false,
|
comparison_color_enabled: comparisonColorEnabled = false,
|
||||||
comparison_color_scheme: comparisonColorScheme = ColorSchemeEnum.Green,
|
comparison_color_scheme: comparisonColorScheme = ColorSchemeEnum.Green,
|
||||||
comparison_type,
|
comparison_type,
|
||||||
slice_id,
|
|
||||||
} = formData;
|
} = formData;
|
||||||
const isUsingTimeComparison =
|
const isUsingTimeComparison =
|
||||||
!isEmpty(time_compare) &&
|
!isEmpty(time_compare) &&
|
||||||
@@ -685,26 +675,6 @@ const transformProps = (
|
|||||||
conditionalFormatting,
|
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;
|
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
||||||
return {
|
return {
|
||||||
height,
|
height,
|
||||||
@@ -712,10 +682,7 @@ const transformProps = (
|
|||||||
isRawRecords: queryMode === QueryMode.Raw,
|
isRawRecords: queryMode === QueryMode.Raw,
|
||||||
data: passedData,
|
data: passedData,
|
||||||
totals,
|
totals,
|
||||||
columns:
|
columns: passedColumns,
|
||||||
Array.isArray(passedColumns) && passedColumns?.length > 0
|
|
||||||
? passedColumns
|
|
||||||
: cachedValues?.passedColumns || [],
|
|
||||||
serverPagination,
|
serverPagination,
|
||||||
metrics,
|
metrics,
|
||||||
percentMetrics,
|
percentMetrics,
|
||||||
@@ -730,9 +697,7 @@ const transformProps = (
|
|||||||
includeSearch,
|
includeSearch,
|
||||||
rowCount,
|
rowCount,
|
||||||
pageSize: serverPagination
|
pageSize: serverPagination
|
||||||
? serverPaginationData?.pageSize
|
? serverPageLength
|
||||||
? serverPaginationData?.pageSize
|
|
||||||
: serverPageLength
|
|
||||||
: getPageSize(pageLength, data.length, columns.length),
|
: getPageSize(pageLength, data.length, columns.length),
|
||||||
filters: filterState.filters,
|
filters: filterState.filters,
|
||||||
emitCrossFilters,
|
emitCrossFilters,
|
||||||
@@ -746,9 +711,6 @@ const transformProps = (
|
|||||||
basicColorFormatters,
|
basicColorFormatters,
|
||||||
startDateOffset,
|
startDateOffset,
|
||||||
basicColorColumnFormatters,
|
basicColorColumnFormatters,
|
||||||
hasServerPageLengthChanged,
|
|
||||||
serverPageLength,
|
|
||||||
slice_id,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -114,32 +114,13 @@ export type BasicColorFormatterType = {
|
|||||||
mainArrow: string;
|
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> {
|
export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
|
||||||
timeGrain?: TimeGranularity;
|
timeGrain?: TimeGranularity;
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
rowCount?: number;
|
rowCount?: number;
|
||||||
serverPagination: boolean;
|
serverPagination: boolean;
|
||||||
serverPaginationData: ServerPaginationData;
|
serverPaginationData: { pageSize?: number; currentPage?: number };
|
||||||
setDataMask: SetDataMaskHook;
|
setDataMask: SetDataMaskHook;
|
||||||
isRawRecords?: boolean;
|
isRawRecords?: boolean;
|
||||||
data: D[];
|
data: D[];
|
||||||
@@ -171,11 +152,6 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
|
|||||||
basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
||||||
basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
||||||
startDateOffset?: string;
|
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 {
|
export enum ColorSchemeEnum {
|
||||||
|
|||||||
@@ -327,10 +327,6 @@ class ChartRenderer extends Component {
|
|||||||
?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
|
?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
|
||||||
? { inContextMenu: this.state.inContextMenu }
|
? { 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -371,7 +367,6 @@ class ChartRenderer extends Component {
|
|||||||
postTransformProps={postTransformProps}
|
postTransformProps={postTransformProps}
|
||||||
emitCrossFilters={emitCrossFilters}
|
emitCrossFilters={emitCrossFilters}
|
||||||
legendState={this.state.legendState}
|
legendState={this.state.legendState}
|
||||||
enableNoResults={bypassNoResult}
|
|
||||||
{...drillToDetailProps}
|
{...drillToDetailProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
t,
|
t,
|
||||||
withTheme,
|
withTheme,
|
||||||
getClientErrorObject,
|
getClientErrorObject,
|
||||||
getExtensionsRegistry,
|
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { Select, AsyncSelect, Row, Col } from 'src/components';
|
import { Select, AsyncSelect, Row, Col } from 'src/components';
|
||||||
import { FormLabel } from 'src/components/Form';
|
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 withToasts from 'src/components/MessageToasts/withToasts';
|
||||||
import { Icons } from 'src/components/Icons';
|
import { Icons } from 'src/components/Icons';
|
||||||
import CurrencyControl from 'src/explore/components/controls/CurrencyControl';
|
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 CollectionTable from './CollectionTable';
|
||||||
import Fieldset from './Fieldset';
|
import Fieldset from './Fieldset';
|
||||||
import Field from './Field';
|
import Field from './Field';
|
||||||
import { fetchSyncedColumns, updateColumns } from './utils';
|
import { fetchSyncedColumns, updateColumns } from './utils';
|
||||||
import FilterableTable from '../FilterableTable';
|
|
||||||
|
|
||||||
const extensionsRegistry = getExtensionsRegistry();
|
|
||||||
|
|
||||||
const DatasourceContainer = styled.div`
|
const DatasourceContainer = styled.div`
|
||||||
.change-warning {
|
.change-warning {
|
||||||
@@ -592,8 +586,6 @@ function OwnersSelector({ datasource, onChange }) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const ResultTable =
|
|
||||||
extensionsRegistry.get('sqleditor.extension.resultTable') ?? FilterableTable;
|
|
||||||
|
|
||||||
class DatasourceEditor extends PureComponent {
|
class DatasourceEditor extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -706,23 +698,6 @@ class DatasourceEditor extends PureComponent {
|
|||||||
this.validate(this.onChange);
|
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() {
|
tableChangeAndSyncMetadata() {
|
||||||
this.validate(() => {
|
this.validate(() => {
|
||||||
this.syncMetadata();
|
this.syncMetadata();
|
||||||
@@ -1103,62 +1078,14 @@ class DatasourceEditor extends PureComponent {
|
|||||||
<TextAreaControl
|
<TextAreaControl
|
||||||
language="sql"
|
language="sql"
|
||||||
offerEditInModal={false}
|
offerEditInModal={false}
|
||||||
minLines={10}
|
minLines={20}
|
||||||
maxLines={Infinity}
|
maxLines={Infinity}
|
||||||
readOnly={!this.state.isEditMode}
|
readOnly={!this.state.isEditMode}
|
||||||
resize="both"
|
resize="both"
|
||||||
tooltipOptions={sqlTooltipOptions}
|
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>
|
</div>
|
||||||
@@ -1539,10 +1466,6 @@ class DatasourceEditor extends PureComponent {
|
|||||||
</DatasourceContainer>
|
</DatasourceContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.resetQuery();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DatasourceEditor.defaultProps = defaultProps;
|
DatasourceEditor.defaultProps = defaultProps;
|
||||||
@@ -1550,14 +1473,4 @@ DatasourceEditor.propTypes = propTypes;
|
|||||||
|
|
||||||
const DataSourceComponent = withTheme(DatasourceEditor);
|
const DataSourceComponent = withTheme(DatasourceEditor);
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
export default withToasts(DataSourceComponent);
|
||||||
runQuery: payload => dispatch(executeQuery(payload)),
|
|
||||||
resetQuery: () => dispatch(resetDatabaseState()),
|
|
||||||
});
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
test: state.queryApi,
|
|
||||||
database: state.database,
|
|
||||||
});
|
|
||||||
export default withToasts(
|
|
||||||
connect(mapStateToProps, mapDispatchToProps)(DataSourceComponent),
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -120,83 +120,71 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
|||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
const dialog = useRef<any>(null);
|
const dialog = useRef<any>(null);
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const buildPayload = (datasource: Record<string, any>) => {
|
const buildPayload = (datasource: Record<string, any>) => ({
|
||||||
const payload: Record<string, any> = {
|
table_name: datasource.table_name,
|
||||||
table_name: datasource.table_name,
|
database_id: datasource.database?.id,
|
||||||
database_id: datasource.database?.id,
|
sql: datasource.sql,
|
||||||
sql: datasource.sql,
|
filter_select_enabled: datasource.filter_select_enabled,
|
||||||
filter_select_enabled: datasource.filter_select_enabled,
|
fetch_values_predicate: datasource.fetch_values_predicate,
|
||||||
fetch_values_predicate: datasource.fetch_values_predicate,
|
schema:
|
||||||
schema:
|
datasource.tableSelector?.schema ||
|
||||||
datasource.tableSelector?.schema ||
|
datasource.databaseSelector?.schema ||
|
||||||
datasource.databaseSelector?.schema ||
|
datasource.schema,
|
||||||
datasource.schema,
|
description: datasource.description,
|
||||||
description: datasource.description,
|
main_dttm_col: datasource.main_dttm_col,
|
||||||
main_dttm_col: datasource.main_dttm_col,
|
normalize_columns: datasource.normalize_columns,
|
||||||
normalize_columns: datasource.normalize_columns,
|
always_filter_main_dttm: datasource.always_filter_main_dttm,
|
||||||
always_filter_main_dttm: datasource.always_filter_main_dttm,
|
offset: datasource.offset,
|
||||||
offset: datasource.offset,
|
default_endpoint: datasource.default_endpoint,
|
||||||
default_endpoint: datasource.default_endpoint,
|
cache_timeout:
|
||||||
cache_timeout:
|
datasource.cache_timeout === '' ? null : datasource.cache_timeout,
|
||||||
datasource.cache_timeout === '' ? null : datasource.cache_timeout,
|
is_sqllab_view: datasource.is_sqllab_view,
|
||||||
is_sqllab_view: datasource.is_sqllab_view,
|
template_params: datasource.template_params,
|
||||||
template_params: datasource.template_params,
|
extra: datasource.extra,
|
||||||
extra: datasource.extra,
|
is_managed_externally: datasource.is_managed_externally,
|
||||||
is_managed_externally: datasource.is_managed_externally,
|
external_url: datasource.external_url,
|
||||||
external_url: datasource.external_url,
|
metrics: datasource?.metrics?.map((metric: DatasetObject['metrics'][0]) => {
|
||||||
metrics: datasource?.metrics?.map(
|
const metricBody: any = {
|
||||||
(metric: DatasetObject['metrics'][0]) => {
|
expression: metric.expression,
|
||||||
const metricBody: any = {
|
description: metric.description,
|
||||||
expression: metric.expression,
|
metric_name: metric.metric_name,
|
||||||
description: metric.description,
|
metric_type: metric.metric_type,
|
||||||
metric_name: metric.metric_name,
|
d3format: metric.d3format || null,
|
||||||
metric_type: metric.metric_type,
|
currency: !isDefined(metric.currency)
|
||||||
d3format: metric.d3format || null,
|
? null
|
||||||
currency: !isDefined(metric.currency)
|
: JSON.stringify(metric.currency),
|
||||||
? null
|
verbose_name: metric.verbose_name,
|
||||||
: JSON.stringify(metric.currency),
|
warning_text: metric.warning_text,
|
||||||
verbose_name: metric.verbose_name,
|
uuid: metric.uuid,
|
||||||
warning_text: metric.warning_text,
|
extra: buildExtraJsonObject(metric),
|
||||||
uuid: metric.uuid,
|
};
|
||||||
extra: buildExtraJsonObject(metric),
|
if (!Number.isNaN(Number(metric.id))) {
|
||||||
};
|
metricBody.id = metric.id;
|
||||||
if (!Number.isNaN(Number(metric.id))) {
|
}
|
||||||
metricBody.id = metric.id;
|
return metricBody;
|
||||||
}
|
}),
|
||||||
return metricBody;
|
columns: datasource?.columns?.map(
|
||||||
},
|
(column: DatasetObject['columns'][0]) => ({
|
||||||
),
|
id: typeof column.id === 'number' ? column.id : undefined,
|
||||||
columns: datasource?.columns?.map(
|
column_name: column.column_name,
|
||||||
(column: DatasetObject['columns'][0]) => ({
|
type: column.type,
|
||||||
id: typeof column.id === 'number' ? column.id : undefined,
|
advanced_data_type: column.advanced_data_type,
|
||||||
column_name: column.column_name,
|
verbose_name: column.verbose_name,
|
||||||
type: column.type,
|
description: column.description,
|
||||||
advanced_data_type: column.advanced_data_type,
|
expression: column.expression,
|
||||||
verbose_name: column.verbose_name,
|
filterable: column.filterable,
|
||||||
description: column.description,
|
groupby: column.groupby,
|
||||||
expression: column.expression,
|
is_active: column.is_active,
|
||||||
filterable: column.filterable,
|
is_dttm: column.is_dttm,
|
||||||
groupby: column.groupby,
|
python_date_format: column.python_date_format || null,
|
||||||
is_active: column.is_active,
|
uuid: column.uuid,
|
||||||
is_dttm: column.is_dttm,
|
extra: buildExtraJsonObject(column),
|
||||||
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,
|
||||||
),
|
),
|
||||||
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 onConfirmSave = async () => {
|
const onConfirmSave = async () => {
|
||||||
// Pull out extra fields into the extra object
|
// Pull out extra fields into the extra object
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|||||||
@@ -29,20 +29,13 @@ const defaultProps = {
|
|||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
compact: false,
|
compact: false,
|
||||||
inline: false,
|
inline: false,
|
||||||
additionalControl: (
|
|
||||||
<input type="button" data-test="mock-text-aditional-control" />
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should render', () => {
|
test('should render', () => {
|
||||||
const { container } = render(<Field {...defaultProps} />);
|
const { container } = render(<Field {...defaultProps} />);
|
||||||
expect(container).toBeInTheDocument();
|
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', () => {
|
test('should call onChange', () => {
|
||||||
const { getByTestId } = render(<Field {...defaultProps} />);
|
const { getByTestId } = render(<Field {...defaultProps} />);
|
||||||
const textArea = getByTestId('mock-text-control');
|
const textArea = getByTestId('mock-text-control');
|
||||||
@@ -54,9 +47,3 @@ test('should render compact', () => {
|
|||||||
render(<Field {...defaultProps} compact />);
|
render(<Field {...defaultProps} compact />);
|
||||||
expect(screen.queryByText(defaultProps.description)).not.toBeInTheDocument();
|
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 { css, SupersetTheme } from '@superset-ui/core';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
import { FormItem, FormLabel } from 'src/components/Form';
|
import { FormItem, FormLabel } from 'src/components/Form';
|
||||||
import { Icons } from 'src/components/Icons';
|
|
||||||
|
|
||||||
const formItemInlineCss = css`
|
const formItemInlineCss = css`
|
||||||
.ant-form-item-control-input-content {
|
.ant-form-item-control-input-content {
|
||||||
@@ -29,17 +28,16 @@ const formItemInlineCss = css`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface FieldProps<V> {
|
interface FieldProps<V> {
|
||||||
fieldKey: string;
|
fieldKey: string;
|
||||||
value?: V;
|
value?: V;
|
||||||
label: string;
|
label: string;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
control: ReactElement;
|
control: ReactElement;
|
||||||
additionalControl?: ReactElement;
|
|
||||||
onChange: (fieldKey: string, newValue: V) => void;
|
onChange: (fieldKey: string, newValue: V) => void;
|
||||||
compact: boolean;
|
compact: boolean;
|
||||||
inline: boolean;
|
inline: boolean;
|
||||||
errorMessage?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Field<V>({
|
export default function Field<V>({
|
||||||
@@ -48,11 +46,9 @@ export default function Field<V>({
|
|||||||
label,
|
label,
|
||||||
description = null,
|
description = null,
|
||||||
control,
|
control,
|
||||||
additionalControl,
|
|
||||||
onChange = () => {},
|
onChange = () => {},
|
||||||
compact = false,
|
compact = false,
|
||||||
inline,
|
inline,
|
||||||
errorMessage,
|
|
||||||
}: FieldProps<V>) {
|
}: FieldProps<V>) {
|
||||||
const onControlChange = useCallback(
|
const onControlChange = useCallback(
|
||||||
newValue => {
|
newValue => {
|
||||||
@@ -66,51 +62,32 @@ export default function Field<V>({
|
|||||||
onChange: onControlChange,
|
onChange: onControlChange,
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div
|
<FormItem
|
||||||
css={
|
label={
|
||||||
additionalControl &&
|
<FormLabel className="m-r-5">
|
||||||
css`
|
{label || fieldKey}
|
||||||
position: relative;
|
{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}
|
{hookedControl}
|
||||||
<FormItem
|
{!compact && description && (
|
||||||
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 && (
|
|
||||||
<div
|
<div
|
||||||
css={(theme: SupersetTheme) => ({
|
css={(theme: SupersetTheme) => ({
|
||||||
color: theme.colors.error.base,
|
color: theme.colors.grayscale.base,
|
||||||
marginTop: -16,
|
[inline ? 'marginLeft' : 'marginTop']: theme.gridUnit,
|
||||||
fontSize: theme.typography.sizes.s,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{errorMessage}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
CaretDownOutlined,
|
CaretDownOutlined,
|
||||||
CaretLeftOutlined,
|
CaretLeftOutlined,
|
||||||
CaretRightOutlined,
|
CaretRightOutlined,
|
||||||
CaretRightFilled,
|
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
@@ -135,7 +134,6 @@ const AntdIcons = {
|
|||||||
CaretDownOutlined,
|
CaretDownOutlined,
|
||||||
CaretLeftOutlined,
|
CaretLeftOutlined,
|
||||||
CaretRightOutlined,
|
CaretRightOutlined,
|
||||||
CaretRightFilled,
|
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
|
|||||||
import { componentShape } from 'src/dashboard/util/propShapes';
|
import { componentShape } from 'src/dashboard/util/propShapes';
|
||||||
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
|
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
|
||||||
import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
|
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 { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants';
|
||||||
import { isCurrentUserBot } from 'src/utils/isBot';
|
import { isCurrentUserBot } from 'src/utils/isBot';
|
||||||
import { useDebouncedEffect } from '../../../explore/exploreUtils';
|
import { useDebouncedEffect } from '../../../explore/exploreUtils';
|
||||||
@@ -189,10 +188,7 @@ const Row = props => {
|
|||||||
observerDisabler = new IntersectionObserver(
|
observerDisabler = new IntersectionObserver(
|
||||||
([entry]) => {
|
([entry]) => {
|
||||||
if (!entry.isIntersecting && isComponentVisibleRef.current) {
|
if (!entry.isIntersecting && isComponentVisibleRef.current) {
|
||||||
// Reference: https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-rootmargin
|
setIsInView(false);
|
||||||
if (!isEmbedded()) {
|
|
||||||
setIsInView(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||||
import { makeApi, t, logging } from '@superset-ui/core';
|
import { makeApi, t, logging } from '@superset-ui/core';
|
||||||
import Switchboard from '@superset-ui/switchboard';
|
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 setupClient from 'src/setup/setupClient';
|
||||||
import setupPlugins from 'src/setup/setupPlugins';
|
import setupPlugins from 'src/setup/setupPlugins';
|
||||||
import { useUiConfig } from 'src/components/UiConfigContext';
|
import { useUiConfig } from 'src/components/UiConfigContext';
|
||||||
@@ -94,7 +94,7 @@ const EmbeddedRoute = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const EmbeddedApp = () => (
|
const EmbeddedApp = () => (
|
||||||
<Router basename={applicationRoot()}>
|
<Router>
|
||||||
{/* todo (embedded) remove this line after uuids are deployed */}
|
{/* todo (embedded) remove this line after uuids are deployed */}
|
||||||
<Route path="/dashboard/:idOrSlug/embedded/" component={EmbeddedRoute} />
|
<Route path="/dashboard/:idOrSlug/embedded/" component={EmbeddedRoute} />
|
||||||
<Route path="/embedded/:uuid/" component={EmbeddedRoute} />
|
<Route path="/embedded/:uuid/" component={EmbeddedRoute} />
|
||||||
@@ -187,7 +187,6 @@ function start() {
|
|||||||
*/
|
*/
|
||||||
function setupGuestClient(guestToken: string) {
|
function setupGuestClient(guestToken: string) {
|
||||||
setupClient({
|
setupClient({
|
||||||
appRoot: applicationRoot(),
|
|
||||||
guestToken,
|
guestToken,
|
||||||
guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
|
guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
|
||||||
unauthorizedHandler: guestUnauthorizedHandler,
|
unauthorizedHandler: guestUnauthorizedHandler,
|
||||||
|
|||||||
@@ -214,52 +214,6 @@ export default function exploreReducer(state = {}, action) {
|
|||||||
currentControlsState = transformed.controlsState;
|
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 {
|
return {
|
||||||
...state,
|
...state,
|
||||||
form_data: new_form_data,
|
form_data: new_form_data,
|
||||||
@@ -273,7 +227,6 @@ export default function exploreReducer(state = {}, action) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
...rerenderedControls,
|
...rerenderedControls,
|
||||||
...updatedControlStates,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ export type DatasetObject = {
|
|||||||
filter_select_enabled?: boolean;
|
filter_select_enabled?: boolean;
|
||||||
fetch_values_predicate?: string;
|
fetch_values_predicate?: string;
|
||||||
schema?: string;
|
schema?: string;
|
||||||
catalog?: string;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
main_dttm_col: string;
|
main_dttm_col: string;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import logger from 'src/middleware/loggerMiddleware';
|
|||||||
import saveModal from 'src/explore/reducers/saveModalReducer';
|
import saveModal from 'src/explore/reducers/saveModalReducer';
|
||||||
import explore from 'src/explore/reducers/exploreReducer';
|
import explore from 'src/explore/reducers/exploreReducer';
|
||||||
import exploreDatasources from 'src/explore/reducers/datasourcesReducer';
|
import exploreDatasources from 'src/explore/reducers/datasourcesReducer';
|
||||||
|
|
||||||
import { persistSqlLabStateEnhancer } from 'src/SqlLab/middlewares/persistSqlLabStateEnhancer';
|
import { persistSqlLabStateEnhancer } from 'src/SqlLab/middlewares/persistSqlLabStateEnhancer';
|
||||||
import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
|
import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
|
||||||
import getInitialState from 'src/SqlLab/reducers/getInitialState';
|
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 { HydrateExplore } from 'src/explore/actions/hydrateExplore';
|
||||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||||
import { Dataset } from '@superset-ui/chart-controls';
|
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".
|
// 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.
|
// This may change later, as the client application takes on more responsibilities.
|
||||||
@@ -139,7 +139,6 @@ const reducers = {
|
|||||||
reports,
|
reports,
|
||||||
saveModal,
|
saveModal,
|
||||||
explore,
|
explore,
|
||||||
database: databaseReducer,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* In some cases the jinja template injects two separate React apps into basic.html
|
/* In some cases the jinja template injects two separate React apps into basic.html
|
||||||
|
|||||||
@@ -33,18 +33,6 @@ def get_dataset_exist_error_msg(table: Table) -> str:
|
|||||||
return _("Dataset %(table)s already exists", table=table)
|
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):
|
class DatabaseNotFoundValidationError(ValidationError):
|
||||||
"""
|
"""
|
||||||
Marshmallow validation error for database does not exist
|
Marshmallow validation error for database does not exist
|
||||||
@@ -54,6 +42,15 @@ class DatabaseNotFoundValidationError(ValidationError):
|
|||||||
super().__init__([_("Database does not exist")], field_name="database")
|
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):
|
class DatasetExistsValidationError(ValidationError):
|
||||||
"""
|
"""
|
||||||
Marshmallow validation error for dataset already exists
|
Marshmallow validation error for dataset already exists
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -28,7 +26,7 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||||||
from superset import is_feature_enabled, security_manager
|
from superset import is_feature_enabled, security_manager
|
||||||
from superset.commands.base import BaseCommand, UpdateMixin
|
from superset.commands.base import BaseCommand, UpdateMixin
|
||||||
from superset.commands.dataset.exceptions import (
|
from superset.commands.dataset.exceptions import (
|
||||||
DatabaseNotFoundValidationError,
|
DatabaseChangeValidationError,
|
||||||
DatasetColumnNotFoundValidationError,
|
DatasetColumnNotFoundValidationError,
|
||||||
DatasetColumnsDuplicateValidationError,
|
DatasetColumnsDuplicateValidationError,
|
||||||
DatasetColumnsExistsValidationError,
|
DatasetColumnsExistsValidationError,
|
||||||
@@ -40,13 +38,11 @@ from superset.commands.dataset.exceptions import (
|
|||||||
DatasetMetricsNotFoundValidationError,
|
DatasetMetricsNotFoundValidationError,
|
||||||
DatasetNotFoundError,
|
DatasetNotFoundError,
|
||||||
DatasetUpdateFailedError,
|
DatasetUpdateFailedError,
|
||||||
MultiCatalogDisabledValidationError,
|
|
||||||
)
|
)
|
||||||
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
|
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
|
||||||
from superset.daos.dataset import DatasetDAO
|
from superset.daos.dataset import DatasetDAO
|
||||||
from superset.datasets.schemas import FolderSchema
|
from superset.datasets.schemas import FolderSchema
|
||||||
from superset.exceptions import SupersetSecurityException
|
from superset.exceptions import SupersetSecurityException
|
||||||
from superset.models.core import Database
|
|
||||||
from superset.sql_parse import Table
|
from superset.sql_parse import Table
|
||||||
from superset.utils.decorators import on_error, transaction
|
from superset.utils.decorators import on_error, transaction
|
||||||
|
|
||||||
@@ -90,12 +86,38 @@ class UpdateDatasetCommand(UpdateMixin, BaseCommand):
|
|||||||
if not self._model:
|
if not self._model:
|
||||||
raise DatasetNotFoundError()
|
raise DatasetNotFoundError()
|
||||||
|
|
||||||
# Check permission to update the dataset
|
# Check ownership
|
||||||
try:
|
try:
|
||||||
security_manager.raise_for_ownership(self._model)
|
security_manager.raise_for_ownership(self._model)
|
||||||
except SupersetSecurityException as ex:
|
except SupersetSecurityException as ex:
|
||||||
raise DatasetForbiddenError() from 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
|
# Validate/Populate owner
|
||||||
try:
|
try:
|
||||||
owners = self.compute_owners(
|
owners = self.compute_owners(
|
||||||
@@ -106,68 +128,15 @@ class UpdateDatasetCommand(UpdateMixin, BaseCommand):
|
|||||||
except ValidationError as ex:
|
except ValidationError as ex:
|
||||||
exceptions.append(ex)
|
exceptions.append(ex)
|
||||||
|
|
||||||
self._validate_dataset_source(exceptions)
|
|
||||||
self._validate_semantics(exceptions)
|
self._validate_semantics(exceptions)
|
||||||
|
|
||||||
if exceptions:
|
if exceptions:
|
||||||
raise DatasetInvalidError(exceptions=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:
|
def _validate_semantics(self, exceptions: list[ValidationError]) -> None:
|
||||||
# we know we have a valid model
|
# we know we have a valid model
|
||||||
self._model = cast(SqlaTable, self._model)
|
self._model = cast(SqlaTable, self._model)
|
||||||
|
|
||||||
if columns := self._properties.get("columns"):
|
if columns := self._properties.get("columns"):
|
||||||
self._validate_columns(columns, exceptions)
|
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_type = result_type or ChartDataResultType.FULL
|
||||||
result_format = result_format or ChartDataResultFormat.JSON
|
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_ = [
|
queries_ = [
|
||||||
self._process_query_object(
|
self._process_query_object(
|
||||||
datasource_model_instance,
|
datasource_model_instance,
|
||||||
form_data,
|
form_data,
|
||||||
self._query_object_factory.create(
|
self._query_object_factory.create(
|
||||||
result_type,
|
result_type, datasource=datasource, **query_obj
|
||||||
datasource=datasource,
|
|
||||||
server_pagination=server_pagination,
|
|
||||||
**query_obj,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for query_obj in queries
|
for query_obj in queries
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
|
|||||||
row_limit: int | None = None,
|
row_limit: int | None = None,
|
||||||
time_range: str | None = None,
|
time_range: str | None = None,
|
||||||
time_shift: str | None = None,
|
time_shift: str | None = None,
|
||||||
server_pagination: bool | None = None,
|
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> QueryObject:
|
) -> QueryObject:
|
||||||
datasource_model_instance = None
|
datasource_model_instance = None
|
||||||
@@ -65,12 +64,7 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
|
|||||||
datasource_model_instance = self._convert_to_model(datasource)
|
datasource_model_instance = self._convert_to_model(datasource)
|
||||||
processed_extras = self._process_extras(extras)
|
processed_extras = self._process_extras(extras)
|
||||||
result_type = kwargs.setdefault("result_type", parent_result_type)
|
result_type = kwargs.setdefault("result_type", parent_result_type)
|
||||||
|
row_limit = self._process_row_limit(row_limit, result_type)
|
||||||
# Process row limit taking server pagination into account
|
|
||||||
row_limit = self._process_row_limit(
|
|
||||||
row_limit, result_type, server_pagination=server_pagination
|
|
||||||
)
|
|
||||||
|
|
||||||
processed_time_range = self._process_time_range(
|
processed_time_range = self._process_time_range(
|
||||||
time_range, kwargs.get("filters"), kwargs.get("columns")
|
time_range, kwargs.get("filters"), kwargs.get("columns")
|
||||||
)
|
)
|
||||||
@@ -102,27 +96,14 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
|
|||||||
return extras
|
return extras
|
||||||
|
|
||||||
def _process_row_limit(
|
def _process_row_limit(
|
||||||
self,
|
self, row_limit: int | None, result_type: ChartDataResultType
|
||||||
row_limit: int | None,
|
|
||||||
result_type: ChartDataResultType,
|
|
||||||
server_pagination: bool | None = None,
|
|
||||||
) -> int:
|
) -> 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 = (
|
default_row_limit = (
|
||||||
self._config["SAMPLES_ROW_LIMIT"]
|
self._config["SAMPLES_ROW_LIMIT"]
|
||||||
if result_type == ChartDataResultType.SAMPLES
|
if result_type == ChartDataResultType.SAMPLES
|
||||||
else self._config["ROW_LIMIT"]
|
else self._config["ROW_LIMIT"]
|
||||||
)
|
)
|
||||||
return apply_max_row_limit(
|
return apply_max_row_limit(row_limit or default_row_limit)
|
||||||
row_limit or default_row_limit,
|
|
||||||
server_pagination=server_pagination,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _process_time_range(
|
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
|
# Maximum number of rows returned for any analytical database query
|
||||||
SQL_MAX_ROW = 100000
|
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
|
# Maximum number of rows displayed in SQL Lab UI
|
||||||
# Is set to avoid out of memory/localstorage issues in browsers. Does not affect
|
# Is set to avoid out of memory/localstorage issues in browsers. Does not affect
|
||||||
# exported CSVs
|
# exported CSVs
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
import polyline
|
import polyline
|
||||||
from sqlalchemy import inspect, String, Text
|
from sqlalchemy import inspect, String, Text
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ from superset.sql_parse import Table
|
|||||||
from superset.utils import json
|
from superset.utils import json
|
||||||
|
|
||||||
from ..utils.database import get_example_database
|
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__)
|
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))
|
table_exists = database.has_table(Table(tbl_name, schema))
|
||||||
|
|
||||||
if not only_metadata and (not table_exists or force):
|
if not only_metadata and (not table_exists or force):
|
||||||
df = read_example_data(
|
url = get_example_url("bart-lines.json.gz")
|
||||||
"bart-lines.json.gz", encoding="latin-1", compression="gzip"
|
df = pd.read_json(url, encoding="latin-1", compression="gzip")
|
||||||
)
|
|
||||||
df["path_json"] = df.path.map(json.dumps)
|
df["path_json"] = df.path.map(json.dumps)
|
||||||
df["polyline"] = df.path.map(polyline.encode)
|
df["polyline"] = df.path.map(polyline.encode)
|
||||||
del df["path"]
|
del df["path"]
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ from superset.utils.core import DatasourceType
|
|||||||
|
|
||||||
from ..utils.database import get_example_database
|
from ..utils.database import get_example_database
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
|
get_example_url,
|
||||||
get_slice_json,
|
get_slice_json,
|
||||||
get_table_connector_registry,
|
get_table_connector_registry,
|
||||||
merge_slice,
|
merge_slice,
|
||||||
misc_dash_slices,
|
misc_dash_slices,
|
||||||
read_example_data,
|
|
||||||
update_slice_ids,
|
update_slice_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,8 +57,8 @@ def gen_filter(
|
|||||||
|
|
||||||
|
|
||||||
def load_data(tbl_name: str, database: Database, sample: bool = False) -> None:
|
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
|
# TODO(bkyryliuk): move load examples data into the pytest fixture
|
||||||
if database.backend == "presto":
|
if database.backend == "presto":
|
||||||
pdf.ds = pd.to_datetime(pdf.ds, unit="ms")
|
pdf.ds = pd.to_datetime(pdf.ds, unit="ms")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
from sqlalchemy import BigInteger, Date, inspect, String
|
from sqlalchemy import BigInteger, Date, inspect, String
|
||||||
from sqlalchemy.sql import column
|
from sqlalchemy.sql import column
|
||||||
|
|
||||||
@@ -28,11 +29,11 @@ from superset.sql_parse import Table
|
|||||||
from superset.utils.core import DatasourceType
|
from superset.utils.core import DatasourceType
|
||||||
|
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
|
get_example_url,
|
||||||
get_slice_json,
|
get_slice_json,
|
||||||
get_table_connector_registry,
|
get_table_connector_registry,
|
||||||
merge_slice,
|
merge_slice,
|
||||||
misc_dash_slices,
|
misc_dash_slices,
|
||||||
read_example_data,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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))
|
table_exists = database.has_table(Table(tbl_name, schema))
|
||||||
|
|
||||||
if not only_metadata and (not table_exists or force):
|
if not only_metadata and (not table_exists or force):
|
||||||
data = read_example_data(
|
url = get_example_url("birth_france_data_for_country_map.csv")
|
||||||
"birth_france_data_for_country_map.csv", encoding="utf-8"
|
data = pd.read_csv(url, encoding="utf-8")
|
||||||
)
|
|
||||||
data["dttm"] = datetime.datetime.now().date()
|
data["dttm"] = datetime.datetime.now().date()
|
||||||
data.to_sql(
|
data.to_sql(
|
||||||
tbl_name,
|
tbl_name,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
from sqlalchemy import Float, inspect, String
|
from sqlalchemy import Float, inspect, String
|
||||||
from sqlalchemy.sql import column
|
from sqlalchemy.sql import column
|
||||||
|
|
||||||
@@ -28,11 +29,11 @@ from superset.sql_parse import Table
|
|||||||
from superset.utils.core import DatasourceType
|
from superset.utils.core import DatasourceType
|
||||||
|
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
|
get_example_url,
|
||||||
get_slice_json,
|
get_slice_json,
|
||||||
get_table_connector_registry,
|
get_table_connector_registry,
|
||||||
merge_slice,
|
merge_slice,
|
||||||
misc_dash_slices,
|
misc_dash_slices,
|
||||||
read_example_data,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -50,7 +51,8 @@ def load_energy(
|
|||||||
table_exists = database.has_table(Table(tbl_name, schema))
|
table_exists = database.has_table(Table(tbl_name, schema))
|
||||||
|
|
||||||
if not only_metadata and (not table_exists or force):
|
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 = pdf.head(100) if sample else pdf
|
||||||
pdf.to_sql(
|
pdf.to_sql(
|
||||||
tbl_name,
|
tbl_name,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import superset.utils.database as database_utils
|
|||||||
from superset import db
|
from superset import db
|
||||||
from superset.sql_parse import Table
|
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__)
|
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))
|
table_exists = database.has_table(Table(tbl_name, schema))
|
||||||
|
|
||||||
if not only_metadata and (not table_exists or force):
|
if not only_metadata and (not table_exists or force):
|
||||||
pdf = read_example_data(
|
flight_data_url = get_example_url("flight_data.csv.gz")
|
||||||
"flight_data.csv.gz", encoding="latin-1", compression="gzip"
|
pdf = pd.read_csv(flight_data_url, encoding="latin-1", compression="gzip")
|
||||||
)
|
|
||||||
|
|
||||||
# Loading airports info to join and get lat/long
|
# Loading airports info to join and get lat/long
|
||||||
airports = read_example_data(
|
airports_url = get_example_url("airports.csv.gz")
|
||||||
"airports.csv.gz", encoding="latin-1", compression="gzip"
|
airports = pd.read_csv(airports_url, encoding="latin-1", compression="gzip")
|
||||||
)
|
|
||||||
airports = airports.set_index("IATA_CODE")
|
airports = airports.set_index("IATA_CODE")
|
||||||
|
|
||||||
pdf[ # pylint: disable=unsupported-assignment-operation,useless-suppression
|
pdf[ # pylint: disable=unsupported-assignment-operation,useless-suppression
|
||||||
|
|||||||
@@ -43,11 +43,7 @@ Environment knobs
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.error import HTTPError
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from superset import app, db
|
from superset import app, db
|
||||||
from superset.connectors.sqla.models import SqlaTable
|
from superset.connectors.sqla.models import SqlaTable
|
||||||
@@ -123,33 +119,3 @@ def get_example_url(filepath: str) -> str:
|
|||||||
paths like ``datasets/examples/slack/messages.csv``.
|
paths like ``datasets/examples/slack/messages.csv``.
|
||||||
"""
|
"""
|
||||||
return f"{BASE_URL}{filepath}"
|
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
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import logging
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
import geohash
|
import geohash
|
||||||
|
import pandas as pd
|
||||||
from sqlalchemy import DateTime, Float, inspect, String
|
from sqlalchemy import DateTime, Float, inspect, String
|
||||||
|
|
||||||
import superset.utils.database as database_utils
|
import superset.utils.database as database_utils
|
||||||
@@ -28,11 +29,11 @@ from superset.sql_parse import Table
|
|||||||
from superset.utils.core import DatasourceType
|
from superset.utils.core import DatasourceType
|
||||||
|
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
|
get_example_url,
|
||||||
get_slice_json,
|
get_slice_json,
|
||||||
get_table_connector_registry,
|
get_table_connector_registry,
|
||||||
merge_slice,
|
merge_slice,
|
||||||
misc_dash_slices,
|
misc_dash_slices,
|
||||||
read_example_data,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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))
|
table_exists = database.has_table(Table(tbl_name, schema))
|
||||||
|
|
||||||
if not only_metadata and (not table_exists or force):
|
if not only_metadata and (not table_exists or force):
|
||||||
pdf = read_example_data(
|
url = get_example_url("san_francisco.csv.gz")
|
||||||
"san_francisco.csv.gz", encoding="utf-8", compression="gzip"
|
pdf = pd.read_csv(url, encoding="utf-8", compression="gzip")
|
||||||
)
|
|
||||||
start = datetime.datetime.now().replace(
|
start = datetime.datetime.now().replace(
|
||||||
hour=0, minute=0, second=0, microsecond=0
|
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 ..utils.database import get_example_database
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
|
get_example_url,
|
||||||
get_slice_json,
|
get_slice_json,
|
||||||
get_table_connector_registry,
|
get_table_connector_registry,
|
||||||
merge_slice,
|
merge_slice,
|
||||||
misc_dash_slices,
|
misc_dash_slices,
|
||||||
read_example_data,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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))
|
table_exists = database.has_table(Table(tbl_name, schema))
|
||||||
|
|
||||||
if not only_metadata and (not table_exists or force):
|
if not only_metadata and (not table_exists or force):
|
||||||
pdf = read_example_data(
|
url = get_example_url("multiformat_time_series.json.gz")
|
||||||
"multiformat_time_series.json.gz", compression="gzip"
|
pdf = pd.read_json(url, compression="gzip")
|
||||||
)
|
|
||||||
|
|
||||||
# TODO(bkyryliuk): move load examples data into the pytest fixture
|
# TODO(bkyryliuk): move load examples data into the pytest fixture
|
||||||
if database.backend == "presto":
|
if database.backend == "presto":
|
||||||
pdf.ds = pd.to_datetime(pdf.ds, unit="s")
|
pdf.ds = pd.to_datetime(pdf.ds, unit="s")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
from sqlalchemy import inspect, String, Text
|
from sqlalchemy import inspect, String, Text
|
||||||
|
|
||||||
import superset.utils.database as database_utils
|
import superset.utils.database as database_utils
|
||||||
@@ -24,7 +25,7 @@ from superset import db
|
|||||||
from superset.sql_parse import Table
|
from superset.sql_parse import Table
|
||||||
from superset.utils import json
|
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__)
|
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))
|
table_exists = database.has_table(Table(tbl_name, schema))
|
||||||
|
|
||||||
if not only_metadata and (not table_exists or force):
|
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["features"] = df.features.map(json.dumps)
|
||||||
|
|
||||||
df.to_sql(
|
df.to_sql(
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ from superset.sql_parse import Table
|
|||||||
from superset.utils.core import DatasourceType
|
from superset.utils.core import DatasourceType
|
||||||
|
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
|
get_example_url,
|
||||||
get_slice_json,
|
get_slice_json,
|
||||||
get_table_connector_registry,
|
get_table_connector_registry,
|
||||||
merge_slice,
|
merge_slice,
|
||||||
read_example_data,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -46,7 +46,8 @@ def load_random_time_series_data(
|
|||||||
table_exists = database.has_table(Table(tbl_name, schema))
|
table_exists = database.has_table(Table(tbl_name, schema))
|
||||||
|
|
||||||
if not only_metadata and (not table_exists or force):
|
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":
|
if database.backend == "presto":
|
||||||
pdf.ds = pd.to_datetime(pdf.ds, unit="s")
|
pdf.ds = pd.to_datetime(pdf.ds, unit="s")
|
||||||
pdf.ds = pdf.ds.dt.strftime("%Y-%m-%d %H:%M%:%S")
|
pdf.ds = pdf.ds.dt.strftime("%Y-%m-%d %H:%M%:%S")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
from sqlalchemy import BigInteger, Float, inspect, Text
|
from sqlalchemy import BigInteger, Float, inspect, Text
|
||||||
|
|
||||||
import superset.utils.database as database_utils
|
import superset.utils.database as database_utils
|
||||||
@@ -24,7 +25,7 @@ from superset import db
|
|||||||
from superset.sql_parse import Table
|
from superset.sql_parse import Table
|
||||||
from superset.utils import json
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -39,7 +40,8 @@ def load_sf_population_polygons(
|
|||||||
table_exists = database.has_table(Table(tbl_name, schema))
|
table_exists = database.has_table(Table(tbl_name, schema))
|
||||||
|
|
||||||
if not only_metadata and (not table_exists or force):
|
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["contour"] = df.contour.map(json.dumps)
|
||||||
|
|
||||||
df.to_sql(
|
df.to_sql(
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ import superset.utils.database
|
|||||||
from superset import app, db
|
from superset import app, db
|
||||||
from superset.connectors.sqla.models import BaseDatasource, SqlMetric
|
from superset.connectors.sqla.models import BaseDatasource, SqlMetric
|
||||||
from superset.examples.helpers import (
|
from superset.examples.helpers import (
|
||||||
|
get_example_url,
|
||||||
get_examples_folder,
|
get_examples_folder,
|
||||||
get_slice_json,
|
get_slice_json,
|
||||||
get_table_connector_registry,
|
get_table_connector_registry,
|
||||||
merge_slice,
|
merge_slice,
|
||||||
misc_dash_slices,
|
misc_dash_slices,
|
||||||
read_example_data,
|
|
||||||
update_slice_ids,
|
update_slice_ids,
|
||||||
)
|
)
|
||||||
from superset.models.dashboard import Dashboard
|
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))
|
table_exists = database.has_table(Table(tbl_name, schema))
|
||||||
|
|
||||||
if not only_metadata and (not table_exists or force):
|
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]
|
pdf.columns = [col.replace(".", "_") for col in pdf.columns]
|
||||||
if database.backend == "presto":
|
if database.backend == "presto":
|
||||||
pdf.year = pd.to_datetime(pdf.year)
|
pdf.year = pd.to_datetime(pdf.year)
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"來確保字符的表達順序與時間順序一致的標準。如果時間戳格式不符合 ISO 8601 "
|
"來確保字符的表達順序與時間順序一致的標準。如果時間戳格式不符合 ISO 8601 "
|
||||||
"標準,则需要定義表達式和類型,以便將字符串轉换為日期或時間戳。注意:當前不支持時區。如果時間以 epoch 格式儲存,請输入 `epoch_s` "
|
"標準,则需要定義表達式和類型,以便將字符串轉换為日期或時間戳。注意:當前不支持時區。如果時間以 epoch 格式儲存,請输入 `epoch_s` "
|
||||||
"or `epoch_ms` 。如果没有指定任何模式,我們可以通過額外的参數在每個 資料庫/欄位 名稱級別上使用可選的預設值。"
|
"or `epoch_ms` 。如果没有指定任何模式,我們可以通過額外的参數在每個資料庫/列名級别上使用可選的預設值。"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid " to add calculated columns"
|
msgid " to add calculated columns"
|
||||||
@@ -155,11 +155,11 @@ msgstr "% 計算"
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "% of parent"
|
msgid "% of parent"
|
||||||
msgstr "% 父類"
|
msgstr "父類"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "% of total"
|
msgid "% of total"
|
||||||
msgstr "% 顯示總計"
|
msgstr "顯示總計"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(dialect)s cannot be used as a data source for security reasons."
|
msgid "%(dialect)s cannot be used as a data source for security reasons."
|
||||||
@@ -278,7 +278,7 @@ msgstr "%s 個選項"
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "%s recipients"
|
msgid "%s recipients"
|
||||||
msgstr "%s 最近"
|
msgstr "最近"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "%s row"
|
msgid "%s row"
|
||||||
@@ -1174,7 +1174,7 @@ msgstr "所有"
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "All %s hidden columns"
|
msgid "All %s hidden columns"
|
||||||
msgstr "所有 %s 隱藏的欄位"
|
msgstr "表的列"
|
||||||
|
|
||||||
msgid "All Text"
|
msgid "All Text"
|
||||||
msgstr "所有文本"
|
msgstr "所有文本"
|
||||||
@@ -1625,7 +1625,7 @@ msgstr "應用的過濾器 (%d)"
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Applied filters (%s)"
|
msgid "Applied filters (%s)"
|
||||||
msgstr "應用的過濾器 (%s)"
|
msgstr "應用的過濾器 (%d)"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Applied filters: %s"
|
msgid "Applied filters: %s"
|
||||||
@@ -3615,7 +3615,7 @@ msgstr "無法從结果後端檢索數據。您需要重新運行原始查詢。
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Data for %s"
|
msgid "Data for %s"
|
||||||
msgstr "給 %s 的數據"
|
msgstr "給 JS 的額外數據"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Data imported"
|
msgid "Data imported"
|
||||||
@@ -3729,7 +3729,7 @@ msgstr "數據集"
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Dataset %(table)s already exists"
|
msgid "Dataset %(table)s already exists"
|
||||||
msgstr "數據集 %(table)s 已存在"
|
msgstr "數據集 %(name)s 已存在"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Dataset Name"
|
msgid "Dataset Name"
|
||||||
@@ -4471,7 +4471,7 @@ msgstr "複製"
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Duplicate role %(name)s"
|
msgid "Duplicate role %(name)s"
|
||||||
msgstr "重复的角色 %(name)s"
|
msgstr "重复的列名%(columns)s"
|
||||||
|
|
||||||
msgid "Duplicate tab"
|
msgid "Duplicate tab"
|
||||||
msgstr "複製選項卡"
|
msgstr "複製選項卡"
|
||||||
@@ -5948,7 +5948,7 @@ msgstr "描述要發送給你的報告"
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Include description to be sent with %s"
|
msgid "Include description to be sent with %s"
|
||||||
msgstr ""
|
msgstr "描述要發送給你的報告"
|
||||||
|
|
||||||
msgid "Include series name as an axis"
|
msgid "Include series name as an axis"
|
||||||
msgstr "包括系列名稱作為軸"
|
msgstr "包括系列名稱作為軸"
|
||||||
@@ -9393,7 +9393,7 @@ msgstr "搜索"
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Search %s records"
|
msgid "Search %s records"
|
||||||
msgstr "搜索 %s 紀錄"
|
msgstr "搜索 / 過濾"
|
||||||
|
|
||||||
msgid "Search / Filter"
|
msgid "Search / Filter"
|
||||||
msgstr "搜索 / 過濾"
|
msgstr "搜索 / 過濾"
|
||||||
@@ -9913,7 +9913,7 @@ msgstr "顯示"
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Show %s entries"
|
msgid "Show %s entries"
|
||||||
msgstr "顯示 %s 指標"
|
msgstr "顯示指標"
|
||||||
|
|
||||||
msgid "Show Bubbles"
|
msgid "Show Bubbles"
|
||||||
msgstr "顯示氣泡"
|
msgstr "顯示氣泡"
|
||||||
@@ -10659,7 +10659,7 @@ msgstr "表名"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"Table [%(table)s] could not be found, please double check your database "
|
"Table [%(table)s] could not be found, please double check your database "
|
||||||
"connection, schema, and table name"
|
"connection, schema, and table name"
|
||||||
msgstr "找不到 [%(table)s] 資料表,請仔細檢查您的資料庫連接、Schema 和 資料表名稱"
|
msgstr "找不到 [%(table_name)s] 表,請仔細檢查您的資料庫連接、Schema 和 表名"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Table actions"
|
msgid "Table actions"
|
||||||
@@ -12968,7 +12968,7 @@ msgstr ""
|
|||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid "Waiting on %s"
|
msgid "Waiting on %s"
|
||||||
msgstr "等待 %s 中"
|
msgstr "顯示 %s 個 總計 %s 個"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Waiting on database..."
|
msgid "Waiting on database..."
|
||||||
@@ -14252,14 +14252,14 @@ msgid ""
|
|||||||
" Lab tabs using this database open. Are you sure you want to continue? "
|
" Lab tabs using this database open. Are you sure you want to continue? "
|
||||||
"Deleting the database will break those objects."
|
"Deleting the database will break those objects."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"已經關聯了 %s 圖表和 %s 看板,並且用戶在該資料庫上打開了 %s 個 SQL "
|
"資料庫 %s 已經關聯了 %s 圖表和 %s 看板,並且用戶在該資料庫上打開了 %s 個 SQL "
|
||||||
"編輯器選項卡。確定要繼續嗎?删除資料庫將破壞這些對象。"
|
"編輯器選項卡。確定要繼續嗎?删除資料庫將破坏這些對象。"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"is linked to %s charts that appear on %s dashboards. Are you sure you "
|
"is linked to %s charts that appear on %s dashboards. Are you sure you "
|
||||||
"want to continue? Deleting the dataset will break those objects."
|
"want to continue? Deleting the dataset will break those objects."
|
||||||
msgstr "已經關聯到 %s 圖表和 %s 看板内。確定要繼續嗎?删除數據集將破壞這些對象。"
|
msgstr "數據集 %s 已經链接到 %s 圖表和 %s 看板内。確定要繼續嗎?删除數據集將破坏這些對象。"
|
||||||
|
|
||||||
msgid "is not"
|
msgid "is not"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def memoized_func(key: str, cache: Cache = cache_manager.cache) -> Callable[...,
|
|||||||
force means whether to force refresh the cache and is treated as False by default,
|
force means whether to force refresh the cache and is treated as False by default,
|
||||||
except force = True is passed to the decorated function.
|
except force = True is passed to the decorated function.
|
||||||
|
|
||||||
timeout of cache is set to CACHE_DEFAULT_TIMEOUT seconds by default,
|
timeout of cache is set to 600 seconds by default,
|
||||||
except cache_timeout = {timeout in seconds} is passed to the decorated function.
|
except cache_timeout = {timeout in seconds} is passed to the decorated function.
|
||||||
|
|
||||||
:param key: a callable function that takes function arguments and returns
|
:param key: a callable function that takes function arguments and returns
|
||||||
@@ -121,9 +121,7 @@ def memoized_func(key: str, cache: Cache = cache_manager.cache) -> Callable[...,
|
|||||||
def wrapped_f(*args: Any, **kwargs: Any) -> Any:
|
def wrapped_f(*args: Any, **kwargs: Any) -> Any:
|
||||||
should_cache = kwargs.pop("cache", True)
|
should_cache = kwargs.pop("cache", True)
|
||||||
force = kwargs.pop("force", False)
|
force = kwargs.pop("force", False)
|
||||||
cache_timeout = kwargs.pop(
|
cache_timeout = kwargs.pop("cache_timeout", 0)
|
||||||
"cache_timeout", app.config["CACHE_DEFAULT_TIMEOUT"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not should_cache:
|
if not should_cache:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|||||||
@@ -1741,30 +1741,24 @@ def parse_boolean_string(bool_str: str | None) -> bool:
|
|||||||
|
|
||||||
def apply_max_row_limit(
|
def apply_max_row_limit(
|
||||||
limit: int,
|
limit: int,
|
||||||
server_pagination: bool | None = None,
|
max_limit: int | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Override row limit based on server pagination setting
|
Override row limit if max global limit is defined
|
||||||
|
|
||||||
:param limit: requested row limit
|
:param limit: requested row limit
|
||||||
:param server_pagination: whether server-side pagination
|
:param max_limit: Maximum allowed row limit
|
||||||
is enabled, defaults to None
|
|
||||||
:return: Capped row limit
|
:return: Capped row limit
|
||||||
|
|
||||||
>>> apply_max_row_limit(600000, server_pagination=True) # Server pagination
|
>>> apply_max_row_limit(100000, 10)
|
||||||
500000
|
10
|
||||||
>>> apply_max_row_limit(600000, server_pagination=False) # No pagination
|
>>> apply_max_row_limit(10, 100000)
|
||||||
50000
|
10
|
||||||
>>> apply_max_row_limit(5000) # No server_pagination specified
|
>>> apply_max_row_limit(0, 10000)
|
||||||
5000
|
10000
|
||||||
>>> apply_max_row_limit(0) # Zero returns default max limit
|
|
||||||
50000
|
|
||||||
"""
|
"""
|
||||||
max_limit = (
|
if max_limit is None:
|
||||||
current_app.config["TABLE_VIZ_MAX_ROW_SERVER"]
|
max_limit = current_app.config["SQL_MAX_ROW"]
|
||||||
if server_pagination
|
|
||||||
else current_app.config["SQL_MAX_ROW"]
|
|
||||||
)
|
|
||||||
if limit != 0:
|
if limit != 0:
|
||||||
return min(max_limit, limit)
|
return min(max_limit, limit)
|
||||||
return max_limit
|
return max_limit
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ FRONTEND_CONF_KEYS = (
|
|||||||
"JWT_ACCESS_CSRF_COOKIE_NAME",
|
"JWT_ACCESS_CSRF_COOKIE_NAME",
|
||||||
"SQLLAB_QUERY_RESULT_TIMEOUT",
|
"SQLLAB_QUERY_RESULT_TIMEOUT",
|
||||||
"SYNC_DB_PERMISSIONS_IN_ASYNC_MODE",
|
"SYNC_DB_PERMISSIONS_IN_ASYNC_MODE",
|
||||||
"TABLE_VIZ_MAX_ROW_SERVER",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -14,10 +14,11 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
from __future__ import annotations
|
"""Unit tests for Superset"""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from typing import Optional
|
||||||
from unittest.mock import ANY, patch
|
from unittest.mock import ANY, patch
|
||||||
from zipfile import is_zipfile, ZipFile
|
from zipfile import is_zipfile, ZipFile
|
||||||
|
|
||||||
@@ -69,26 +70,14 @@ from tests.integration_tests.fixtures.importexport import (
|
|||||||
class TestDatasetApi(SupersetTestCase):
|
class TestDatasetApi(SupersetTestCase):
|
||||||
fixture_tables_names = ("ab_permission", "ab_permission_view", "ab_view_menu")
|
fixture_tables_names = ("ab_permission", "ab_permission_view", "ab_view_menu")
|
||||||
fixture_virtual_table_names = ("sql_virtual_dataset_1", "sql_virtual_dataset_2")
|
fixture_virtual_table_names = ("sql_virtual_dataset_1", "sql_virtual_dataset_2")
|
||||||
items_to_delete: list[SqlaTable | Database | TableColumn] = []
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.items_to_delete = []
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
for item in self.items_to_delete:
|
|
||||||
db.session.delete(item)
|
|
||||||
db.session.commit()
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def insert_dataset(
|
def insert_dataset(
|
||||||
table_name: str,
|
table_name: str,
|
||||||
owners: list[int],
|
owners: list[int],
|
||||||
database: Database,
|
database: Database,
|
||||||
sql: str | None = None,
|
sql: Optional[str] = None,
|
||||||
schema: str | None = None,
|
schema: Optional[str] = None,
|
||||||
catalog: str | None = None,
|
|
||||||
fetch_metadata: bool = True,
|
|
||||||
) -> SqlaTable:
|
) -> SqlaTable:
|
||||||
obj_owners = list() # noqa: C408
|
obj_owners = list() # noqa: C408
|
||||||
for owner in owners:
|
for owner in owners:
|
||||||
@@ -100,12 +89,10 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
owners=obj_owners,
|
owners=obj_owners,
|
||||||
database=database,
|
database=database,
|
||||||
sql=sql,
|
sql=sql,
|
||||||
catalog=catalog,
|
|
||||||
)
|
)
|
||||||
db.session.add(table)
|
db.session.add(table)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
if fetch_metadata:
|
table.fetch_metadata()
|
||||||
table.fetch_metadata()
|
|
||||||
return table
|
return table
|
||||||
|
|
||||||
def insert_default_dataset(self):
|
def insert_default_dataset(self):
|
||||||
@@ -113,16 +100,6 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
"ab_permission", [self.get_user("admin").id], get_main_database()
|
"ab_permission", [self.get_user("admin").id], get_main_database()
|
||||||
)
|
)
|
||||||
|
|
||||||
def insert_database(self, name: str, allow_multi_catalog: bool = False) -> Database:
|
|
||||||
db_connection = Database(
|
|
||||||
database_name=name,
|
|
||||||
sqlalchemy_uri=get_example_database().sqlalchemy_uri,
|
|
||||||
extra=('{"allow_multi_catalog": true}' if allow_multi_catalog else "{}"),
|
|
||||||
)
|
|
||||||
db.session.add(db_connection)
|
|
||||||
db.session.commit()
|
|
||||||
return db_connection
|
|
||||||
|
|
||||||
def get_fixture_datasets(self) -> list[SqlaTable]:
|
def get_fixture_datasets(self) -> list[SqlaTable]:
|
||||||
return (
|
return (
|
||||||
db.session.query(SqlaTable)
|
db.session.query(SqlaTable)
|
||||||
@@ -338,7 +315,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
|
|
||||||
# revert gamma permission
|
# revert gamma permission
|
||||||
gamma_role.permissions.remove(main_db_pvm)
|
gamma_role.permissions.remove(main_db_pvm)
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_get_dataset_related_database_gamma(self):
|
def test_get_dataset_related_database_gamma(self):
|
||||||
"""
|
"""
|
||||||
@@ -502,7 +480,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_get_dataset_render_jinja_exceptions(self):
|
def test_get_dataset_render_jinja_exceptions(self):
|
||||||
"""
|
"""
|
||||||
@@ -568,7 +547,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
== "Unable to render expression from dataset calculated column."
|
== "Unable to render expression from dataset calculated column."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_get_dataset_distinct_schema(self):
|
def test_get_dataset_distinct_schema(self):
|
||||||
"""
|
"""
|
||||||
@@ -638,7 +618,9 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.items_to_delete = datasets
|
for dataset in datasets:
|
||||||
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_get_dataset_distinct_not_allowed(self):
|
def test_get_dataset_distinct_not_allowed(self):
|
||||||
"""
|
"""
|
||||||
@@ -665,7 +647,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert response["count"] == 0
|
assert response["count"] == 0
|
||||||
assert response["result"] == []
|
assert response["result"] == []
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_get_dataset_info(self):
|
def test_get_dataset_info(self):
|
||||||
"""
|
"""
|
||||||
@@ -739,7 +722,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
)
|
)
|
||||||
assert columns[0].expression == "COUNT(*)"
|
assert columns[0].expression == "COUNT(*)"
|
||||||
|
|
||||||
self.items_to_delete = [model]
|
db.session.delete(model)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_create_dataset_item_normalize(self):
|
def test_create_dataset_item_normalize(self):
|
||||||
"""
|
"""
|
||||||
@@ -765,7 +749,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert model.database_id == table_data["database"]
|
assert model.database_id == table_data["database"]
|
||||||
assert model.normalize_columns is True
|
assert model.normalize_columns is True
|
||||||
|
|
||||||
self.items_to_delete = [model]
|
db.session.delete(model)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_create_dataset_item_gamma(self):
|
def test_create_dataset_item_gamma(self):
|
||||||
"""
|
"""
|
||||||
@@ -806,7 +791,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
model = db.session.query(SqlaTable).get(data.get("id"))
|
model = db.session.query(SqlaTable).get(data.get("id"))
|
||||||
assert admin in model.owners
|
assert admin in model.owners
|
||||||
assert alpha in model.owners
|
assert alpha in model.owners
|
||||||
self.items_to_delete = [model]
|
db.session.delete(model)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_create_dataset_item_owners_invalid(self):
|
def test_create_dataset_item_owners_invalid(self):
|
||||||
"""
|
"""
|
||||||
@@ -853,7 +839,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
model = db.session.query(SqlaTable).get(data.get("id"))
|
model = db.session.query(SqlaTable).get(data.get("id"))
|
||||||
assert admin in model.owners
|
assert admin in model.owners
|
||||||
assert alpha in model.owners
|
assert alpha in model.owners
|
||||||
self.items_to_delete = [model]
|
db.session.delete(model)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@unittest.skip("test is failing stochastically")
|
@unittest.skip("test is failing stochastically")
|
||||||
def test_create_dataset_same_name_different_schema(self):
|
def test_create_dataset_same_name_different_schema(self):
|
||||||
@@ -1004,7 +991,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
model = db.session.query(SqlaTable).get(dataset.id)
|
model = db.session.query(SqlaTable).get(dataset.id)
|
||||||
assert model.owners == current_owners
|
assert model.owners == current_owners
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_clear_owner_list(self):
|
def test_update_dataset_clear_owner_list(self):
|
||||||
"""
|
"""
|
||||||
@@ -1020,7 +1008,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
model = db.session.query(SqlaTable).get(dataset.id)
|
model = db.session.query(SqlaTable).get(dataset.id)
|
||||||
assert model.owners == []
|
assert model.owners == []
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_populate_owner(self):
|
def test_update_dataset_populate_owner(self):
|
||||||
"""
|
"""
|
||||||
@@ -1037,7 +1026,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
model = db.session.query(SqlaTable).get(dataset.id)
|
model = db.session.query(SqlaTable).get(dataset.id)
|
||||||
assert model.owners == [gamma]
|
assert model.owners == [gamma]
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_item(self):
|
def test_update_dataset_item(self):
|
||||||
"""
|
"""
|
||||||
@@ -1055,7 +1045,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert model.description == dataset_data["description"]
|
assert model.description == dataset_data["description"]
|
||||||
assert model.owners == current_owners
|
assert model.owners == current_owners
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_item_w_override_columns(self):
|
def test_update_dataset_item_w_override_columns(self):
|
||||||
"""
|
"""
|
||||||
@@ -1091,7 +1082,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
col.advanced_data_type for col in columns
|
col.advanced_data_type for col in columns
|
||||||
]
|
]
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_item_w_override_columns_same_columns(self):
|
def test_update_dataset_item_w_override_columns_same_columns(self):
|
||||||
"""
|
"""
|
||||||
@@ -1138,7 +1130,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
columns = db.session.query(TableColumn).filter_by(table_id=dataset.id).all()
|
columns = db.session.query(TableColumn).filter_by(table_id=dataset.id).all()
|
||||||
assert len(columns) != prev_col_len
|
assert len(columns) != prev_col_len
|
||||||
assert len(columns) == 3
|
assert len(columns) == 3
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_create_column_and_metric(self):
|
def test_update_dataset_create_column_and_metric(self):
|
||||||
"""
|
"""
|
||||||
@@ -1233,7 +1226,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert metrics[1].warning_text == new_metric_data["warning_text"]
|
assert metrics[1].warning_text == new_metric_data["warning_text"]
|
||||||
assert str(metrics[1].uuid) == new_metric_data["uuid"]
|
assert str(metrics[1].uuid) == new_metric_data["uuid"]
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_delete_column(self):
|
def test_update_dataset_delete_column(self):
|
||||||
"""
|
"""
|
||||||
@@ -1282,7 +1276,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert columns[1].column_name == "name"
|
assert columns[1].column_name == "name"
|
||||||
assert len(columns) == 2
|
assert len(columns) == 2
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_update_column(self):
|
def test_update_dataset_update_column(self):
|
||||||
"""
|
"""
|
||||||
@@ -1318,7 +1313,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert columns[0].groupby is False
|
assert columns[0].groupby is False
|
||||||
assert columns[0].filterable is False
|
assert columns[0].filterable is False
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_delete_metric(self):
|
def test_update_dataset_delete_metric(self):
|
||||||
"""
|
"""
|
||||||
@@ -1361,7 +1357,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
metrics = metrics_query.all()
|
metrics = metrics_query.all()
|
||||||
assert len(metrics) == 1
|
assert len(metrics) == 1
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_update_column_uniqueness(self):
|
def test_update_dataset_update_column_uniqueness(self):
|
||||||
"""
|
"""
|
||||||
@@ -1381,7 +1378,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
"message": {"columns": ["One or more columns already exist"]}
|
"message": {"columns": ["One or more columns already exist"]}
|
||||||
}
|
}
|
||||||
assert data == expected_result
|
assert data == expected_result
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_update_metric_uniqueness(self):
|
def test_update_dataset_update_metric_uniqueness(self):
|
||||||
"""
|
"""
|
||||||
@@ -1401,7 +1399,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
"message": {"metrics": ["One or more metrics already exist"]}
|
"message": {"metrics": ["One or more metrics already exist"]}
|
||||||
}
|
}
|
||||||
assert data == expected_result
|
assert data == expected_result
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_update_column_duplicate(self):
|
def test_update_dataset_update_column_duplicate(self):
|
||||||
"""
|
"""
|
||||||
@@ -1426,7 +1425,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
"message": {"columns": ["One or more columns are duplicated"]}
|
"message": {"columns": ["One or more columns are duplicated"]}
|
||||||
}
|
}
|
||||||
assert data == expected_result
|
assert data == expected_result
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_update_metric_duplicate(self):
|
def test_update_dataset_update_metric_duplicate(self):
|
||||||
"""
|
"""
|
||||||
@@ -1451,7 +1451,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
"message": {"metrics": ["One or more metrics are duplicated"]}
|
"message": {"metrics": ["One or more metrics are duplicated"]}
|
||||||
}
|
}
|
||||||
assert data == expected_result
|
assert data == expected_result
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_item_gamma(self):
|
def test_update_dataset_item_gamma(self):
|
||||||
"""
|
"""
|
||||||
@@ -1464,7 +1465,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
uri = f"api/v1/dataset/{dataset.id}"
|
uri = f"api/v1/dataset/{dataset.id}"
|
||||||
rv = self.client.put(uri, json=table_data)
|
rv = self.client.put(uri, json=table_data)
|
||||||
assert rv.status_code == 403
|
assert rv.status_code == 403
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_dataset_get_list_no_username(self):
|
def test_dataset_get_list_no_username(self):
|
||||||
"""
|
"""
|
||||||
@@ -1489,7 +1491,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert current_dataset["description"] == "changed_description"
|
assert current_dataset["description"] == "changed_description"
|
||||||
assert "username" not in current_dataset["changed_by"].keys()
|
assert "username" not in current_dataset["changed_by"].keys()
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_dataset_get_no_username(self):
|
def test_dataset_get_no_username(self):
|
||||||
"""
|
"""
|
||||||
@@ -1509,7 +1512,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert res["description"] == "changed_description"
|
assert res["description"] == "changed_description"
|
||||||
assert "username" not in res["changed_by"].keys()
|
assert "username" not in res["changed_by"].keys()
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_item_not_owned(self):
|
def test_update_dataset_item_not_owned(self):
|
||||||
"""
|
"""
|
||||||
@@ -1522,7 +1526,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
uri = f"api/v1/dataset/{dataset.id}"
|
uri = f"api/v1/dataset/{dataset.id}"
|
||||||
rv = self.put_assert_metric(uri, table_data, "put")
|
rv = self.put_assert_metric(uri, table_data, "put")
|
||||||
assert rv.status_code == 403
|
assert rv.status_code == 403
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_item_owners_invalid(self):
|
def test_update_dataset_item_owners_invalid(self):
|
||||||
"""
|
"""
|
||||||
@@ -1535,7 +1540,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
uri = f"api/v1/dataset/{dataset.id}"
|
uri = f"api/v1/dataset/{dataset.id}"
|
||||||
rv = self.put_assert_metric(uri, table_data, "put")
|
rv = self.put_assert_metric(uri, table_data, "put")
|
||||||
assert rv.status_code == 422
|
assert rv.status_code == 422
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@patch("superset.daos.dataset.DatasetDAO.update")
|
@patch("superset.daos.dataset.DatasetDAO.update")
|
||||||
def test_update_dataset_sqlalchemy_error(self, mock_dao_update):
|
def test_update_dataset_sqlalchemy_error(self, mock_dao_update):
|
||||||
@@ -1554,7 +1560,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert rv.status_code == 422
|
assert rv.status_code == 422
|
||||||
assert data == {"message": "Dataset could not be updated."}
|
assert data == {"message": "Dataset could not be updated."}
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@with_feature_flags(DATASET_FOLDERS=True)
|
@with_feature_flags(DATASET_FOLDERS=True)
|
||||||
def test_update_dataset_add_folders(self):
|
def test_update_dataset_add_folders(self):
|
||||||
@@ -1600,6 +1607,7 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
|
|
||||||
uri = f"api/v1/dataset/{dataset.id}"
|
uri = f"api/v1/dataset/{dataset.id}"
|
||||||
rv = self.put_assert_metric(uri, dataset_data, "put")
|
rv = self.put_assert_metric(uri, dataset_data, "put")
|
||||||
|
print(rv.data.decode("utf-8"))
|
||||||
assert rv.status_code == 200
|
assert rv.status_code == 200
|
||||||
|
|
||||||
model = db.session.query(SqlaTable).get(dataset.id)
|
model = db.session.query(SqlaTable).get(dataset.id)
|
||||||
@@ -1635,229 +1643,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
def test_update_dataset_change_db_connection_multi_catalog_disabled(self):
|
|
||||||
"""
|
|
||||||
Dataset API: Test changing the DB connection powering the dataset
|
|
||||||
to a connection with multi-catalog disabled.
|
|
||||||
"""
|
|
||||||
self.login(ADMIN_USERNAME)
|
|
||||||
|
|
||||||
db_connection = self.insert_database("db_connection")
|
|
||||||
new_db_connection = self.insert_database("new_db_connection")
|
|
||||||
dataset = self.insert_dataset(
|
|
||||||
table_name="test_dataset",
|
|
||||||
owners=[],
|
|
||||||
database=db_connection,
|
|
||||||
sql="select 1 as one",
|
|
||||||
schema="test_schema",
|
|
||||||
catalog="old_default_catalog",
|
|
||||||
fetch_metadata=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch.object(
|
|
||||||
new_db_connection, "get_default_catalog", return_value="new_default_catalog"
|
|
||||||
):
|
|
||||||
payload = {"database_id": new_db_connection.id}
|
|
||||||
uri = f"api/v1/dataset/{dataset.id}"
|
|
||||||
rv = self.put_assert_metric(uri, payload, "put")
|
|
||||||
assert rv.status_code == 200
|
|
||||||
|
|
||||||
model = db.session.query(SqlaTable).get(dataset.id)
|
|
||||||
assert model.database == new_db_connection
|
|
||||||
# Catalog should have been updated to new connection's default catalog
|
|
||||||
assert model.catalog == "new_default_catalog"
|
|
||||||
|
|
||||||
self.items_to_delete = [dataset, db_connection, new_db_connection]
|
|
||||||
|
|
||||||
def test_update_dataset_change_db_connection_multi_catalog_enabled(self):
|
|
||||||
"""
|
|
||||||
Dataset API: Test changing the DB connection powering the dataset
|
|
||||||
to a connection with multi-catalog enabled.
|
|
||||||
"""
|
|
||||||
self.login(ADMIN_USERNAME)
|
|
||||||
|
|
||||||
db_connection = self.insert_database("db_connection")
|
|
||||||
new_db_connection = self.insert_database(
|
|
||||||
"new_db_connection", allow_multi_catalog=True
|
|
||||||
)
|
|
||||||
dataset = self.insert_dataset(
|
|
||||||
table_name="test_dataset",
|
|
||||||
owners=[],
|
|
||||||
database=db_connection,
|
|
||||||
sql="select 1 as one",
|
|
||||||
schema="test_schema",
|
|
||||||
catalog="old_default_catalog",
|
|
||||||
fetch_metadata=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch.object(
|
|
||||||
new_db_connection, "get_default_catalog", return_value="default"
|
|
||||||
):
|
|
||||||
payload = {"database_id": new_db_connection.id}
|
|
||||||
uri = f"api/v1/dataset/{dataset.id}"
|
|
||||||
rv = self.put_assert_metric(uri, payload, "put")
|
|
||||||
assert rv.status_code == 200
|
|
||||||
|
|
||||||
model = db.session.query(SqlaTable).get(dataset.id)
|
|
||||||
assert model.database == new_db_connection
|
|
||||||
# Catalog was not changed as not provided and multi-catalog is enabled
|
|
||||||
assert model.catalog == "old_default_catalog"
|
|
||||||
|
|
||||||
self.items_to_delete = [dataset, db_connection, new_db_connection]
|
|
||||||
|
|
||||||
def test_update_dataset_change_db_connection_not_found(self):
|
|
||||||
"""
|
|
||||||
Dataset API: Test changing the DB connection powering the dataset
|
|
||||||
to an invalid DB connection.
|
|
||||||
"""
|
|
||||||
self.login(ADMIN_USERNAME)
|
|
||||||
|
|
||||||
dataset = self.insert_default_dataset()
|
|
||||||
|
|
||||||
payload = {"database_id": 1500}
|
|
||||||
uri = f"api/v1/dataset/{dataset.id}"
|
|
||||||
rv = self.put_assert_metric(uri, payload, "put")
|
|
||||||
response = json.loads(rv.data.decode("utf-8"))
|
|
||||||
assert rv.status_code == 422
|
|
||||||
assert response["message"] == {"database": ["Database does not exist"]}
|
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
|
||||||
|
|
||||||
def test_update_dataset_change_catalog(self):
|
|
||||||
"""
|
|
||||||
Dataset API: Test changing the catalog associated with the dataset.
|
|
||||||
"""
|
|
||||||
self.login(ADMIN_USERNAME)
|
|
||||||
|
|
||||||
db_connection = self.insert_database("db_connection", allow_multi_catalog=True)
|
|
||||||
dataset = self.insert_dataset(
|
|
||||||
table_name="test_dataset",
|
|
||||||
owners=[],
|
|
||||||
database=db_connection,
|
|
||||||
sql="select 1 as one",
|
|
||||||
schema="test_schema",
|
|
||||||
catalog="test_catalog",
|
|
||||||
fetch_metadata=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch.object(db_connection, "get_default_catalog", return_value="default"):
|
|
||||||
payload = {"catalog": "other_catalog"}
|
|
||||||
uri = f"api/v1/dataset/{dataset.id}"
|
|
||||||
rv = self.put_assert_metric(uri, payload, "put")
|
|
||||||
assert rv.status_code == 200
|
|
||||||
|
|
||||||
model = db.session.query(SqlaTable).get(dataset.id)
|
|
||||||
assert model.catalog == "other_catalog"
|
|
||||||
|
|
||||||
self.items_to_delete = [dataset, db_connection]
|
|
||||||
|
|
||||||
def test_update_dataset_change_catalog_not_allowed(self):
|
|
||||||
"""
|
|
||||||
Dataset API: Test changing the catalog associated with the dataset fails
|
|
||||||
when multi-catalog is disabled on the DB connection.
|
|
||||||
"""
|
|
||||||
self.login(ADMIN_USERNAME)
|
|
||||||
|
|
||||||
db_connection = self.insert_database("db_connection")
|
|
||||||
dataset = self.insert_dataset(
|
|
||||||
table_name="test_dataset",
|
|
||||||
owners=[],
|
|
||||||
database=db_connection,
|
|
||||||
sql="select 1 as one",
|
|
||||||
schema="test_schema",
|
|
||||||
catalog="test_catalog",
|
|
||||||
fetch_metadata=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch.object(db_connection, "get_default_catalog", return_value="default"):
|
|
||||||
payload = {"catalog": "other_catalog"}
|
|
||||||
uri = f"api/v1/dataset/{dataset.id}"
|
|
||||||
rv = self.put_assert_metric(uri, payload, "put")
|
|
||||||
response = json.loads(rv.data.decode("utf-8"))
|
|
||||||
assert rv.status_code == 422
|
|
||||||
assert response["message"] == {
|
|
||||||
"catalog": ["Only the default catalog is supported for this connection"]
|
|
||||||
}
|
|
||||||
|
|
||||||
self.items_to_delete = [dataset, db_connection]
|
|
||||||
|
|
||||||
def test_update_dataset_validate_uniqueness(self):
|
|
||||||
"""
|
|
||||||
Dataset API: Test the dataset uniqueness validation takes into
|
|
||||||
consideration the new database connection.
|
|
||||||
"""
|
|
||||||
test_db = get_main_database()
|
|
||||||
if test_db.backend == "sqlite":
|
|
||||||
# Skip this test for SQLite as it doesn't support multiple
|
|
||||||
# schemas.
|
|
||||||
return
|
|
||||||
|
|
||||||
self.login(ADMIN_USERNAME)
|
|
||||||
|
|
||||||
db_connection = self.insert_database("db_connection")
|
|
||||||
new_db_connection = self.insert_database("new_db_connection")
|
|
||||||
first_schema_dataset = self.insert_dataset(
|
|
||||||
table_name="test_dataset",
|
|
||||||
owners=[],
|
|
||||||
database=db_connection,
|
|
||||||
sql="select 1 as one",
|
|
||||||
schema="first_schema",
|
|
||||||
fetch_metadata=False,
|
|
||||||
)
|
|
||||||
second_schema_dataset = self.insert_dataset(
|
|
||||||
table_name="test_dataset",
|
|
||||||
owners=[],
|
|
||||||
database=db_connection,
|
|
||||||
sql="select 1 as one",
|
|
||||||
schema="second_schema",
|
|
||||||
fetch_metadata=False,
|
|
||||||
)
|
|
||||||
new_db_conn_dataset = self.insert_dataset(
|
|
||||||
table_name="test_dataset",
|
|
||||||
owners=[],
|
|
||||||
database=new_db_connection,
|
|
||||||
sql="select 1 as one",
|
|
||||||
schema="first_schema",
|
|
||||||
fetch_metadata=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch.object(
|
|
||||||
db_connection,
|
|
||||||
"get_default_catalog",
|
|
||||||
return_value=None,
|
|
||||||
):
|
|
||||||
payload = {"schema": "second_schema"}
|
|
||||||
uri = f"api/v1/dataset/{first_schema_dataset.id}"
|
|
||||||
rv = self.put_assert_metric(uri, payload, "put")
|
|
||||||
response = json.loads(rv.data.decode("utf-8"))
|
|
||||||
assert rv.status_code == 422
|
|
||||||
assert response["message"] == {
|
|
||||||
"table": ["Dataset second_schema.test_dataset already exists"]
|
|
||||||
}
|
|
||||||
|
|
||||||
with patch.object(
|
|
||||||
new_db_connection,
|
|
||||||
"get_default_catalog",
|
|
||||||
return_value=None,
|
|
||||||
):
|
|
||||||
payload["database_id"] = new_db_connection.id
|
|
||||||
uri = f"api/v1/dataset/{first_schema_dataset.id}"
|
|
||||||
rv = self.put_assert_metric(uri, payload, "put")
|
|
||||||
assert rv.status_code == 200
|
|
||||||
|
|
||||||
model = db.session.query(SqlaTable).get(first_schema_dataset.id)
|
|
||||||
assert model.database == new_db_connection
|
|
||||||
assert model.schema == "second_schema"
|
|
||||||
|
|
||||||
self.items_to_delete = [
|
|
||||||
first_schema_dataset,
|
|
||||||
second_schema_dataset,
|
|
||||||
new_db_conn_dataset,
|
|
||||||
new_db_connection,
|
|
||||||
db_connection,
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_delete_dataset_item(self):
|
def test_delete_dataset_item(self):
|
||||||
"""
|
"""
|
||||||
@@ -1887,7 +1674,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
uri = f"api/v1/dataset/{dataset.id}"
|
uri = f"api/v1/dataset/{dataset.id}"
|
||||||
rv = self.delete_assert_metric(uri, "delete")
|
rv = self.delete_assert_metric(uri, "delete")
|
||||||
assert rv.status_code == 403
|
assert rv.status_code == 403
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_delete_dataset_item_not_authorized(self):
|
def test_delete_dataset_item_not_authorized(self):
|
||||||
"""
|
"""
|
||||||
@@ -1899,7 +1687,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
uri = f"api/v1/dataset/{dataset.id}"
|
uri = f"api/v1/dataset/{dataset.id}"
|
||||||
rv = self.client.delete(uri)
|
rv = self.client.delete(uri)
|
||||||
assert rv.status_code == 403
|
assert rv.status_code == 403
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@patch("superset.daos.dataset.DatasetDAO.delete")
|
@patch("superset.daos.dataset.DatasetDAO.delete")
|
||||||
def test_delete_dataset_sqlalchemy_error(self, mock_dao_delete):
|
def test_delete_dataset_sqlalchemy_error(self, mock_dao_delete):
|
||||||
@@ -1916,7 +1705,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
data = json.loads(rv.data.decode("utf-8"))
|
data = json.loads(rv.data.decode("utf-8"))
|
||||||
assert rv.status_code == 422
|
assert rv.status_code == 422
|
||||||
assert data == {"message": "Datasets could not be deleted."}
|
assert data == {"message": "Datasets could not be deleted."}
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@pytest.mark.usefixtures("create_datasets")
|
@pytest.mark.usefixtures("create_datasets")
|
||||||
def test_delete_dataset_column(self):
|
def test_delete_dataset_column(self):
|
||||||
@@ -2157,7 +1947,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
.filter_by(table_id=dataset.id, column_name="id")
|
.filter_by(table_id=dataset.id, column_name="id")
|
||||||
.one()
|
.one()
|
||||||
)
|
)
|
||||||
self.items_to_delete = [id_column]
|
db.session.delete(id_column)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
self.login(ADMIN_USERNAME)
|
self.login(ADMIN_USERNAME)
|
||||||
uri = f"api/v1/dataset/{dataset.id}/refresh"
|
uri = f"api/v1/dataset/{dataset.id}/refresh"
|
||||||
@@ -2170,7 +1961,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
.one()
|
.one()
|
||||||
)
|
)
|
||||||
assert id_column is not None
|
assert id_column is not None
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_dataset_item_refresh_not_found(self):
|
def test_dataset_item_refresh_not_found(self):
|
||||||
"""
|
"""
|
||||||
@@ -2195,7 +1987,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
rv = self.put_assert_metric(uri, {}, "refresh")
|
rv = self.put_assert_metric(uri, {}, "refresh")
|
||||||
assert rv.status_code == 403
|
assert rv.status_code == 403
|
||||||
|
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@unittest.skip("test is failing stochastically")
|
@unittest.skip("test is failing stochastically")
|
||||||
def test_export_dataset(self):
|
def test_export_dataset(self):
|
||||||
@@ -2457,7 +2250,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
dataset = (
|
dataset = (
|
||||||
db.session.query(SqlaTable).filter_by(table_name="birth_names_2").one()
|
db.session.query(SqlaTable).filter_by(table_name="birth_names_2").one()
|
||||||
)
|
)
|
||||||
self.items_to_delete = [dataset]
|
db.session.delete(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@patch("superset.commands.database.importers.v1.utils.add_permissions")
|
@patch("superset.commands.database.importers.v1.utils.add_permissions")
|
||||||
def test_import_dataset_overwrite(self, mock_add_permissions):
|
def test_import_dataset_overwrite(self, mock_add_permissions):
|
||||||
@@ -2653,7 +2447,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
response = json.loads(rv.data.decode("utf-8"))
|
response = json.loads(rv.data.decode("utf-8"))
|
||||||
assert response.get("count") == 1
|
assert response.get("count") == 1
|
||||||
|
|
||||||
self.items_to_delete = [table_w_certification]
|
db.session.delete(table_w_certification)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@pytest.mark.usefixtures("create_virtual_datasets")
|
@pytest.mark.usefixtures("create_virtual_datasets")
|
||||||
def test_duplicate_virtual_dataset(self):
|
def test_duplicate_virtual_dataset(self):
|
||||||
@@ -2678,7 +2473,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert len(new_dataset.columns) == 2
|
assert len(new_dataset.columns) == 2
|
||||||
assert new_dataset.columns[0].column_name == "id"
|
assert new_dataset.columns[0].column_name == "id"
|
||||||
assert new_dataset.columns[1].column_name == "name"
|
assert new_dataset.columns[1].column_name == "name"
|
||||||
self.items_to_delete = [new_dataset]
|
db.session.delete(new_dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@pytest.mark.usefixtures("create_datasets")
|
@pytest.mark.usefixtures("create_datasets")
|
||||||
def test_duplicate_physical_dataset(self):
|
def test_duplicate_physical_dataset(self):
|
||||||
@@ -2808,7 +2604,8 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
assert table.template_params == '{"param": 1}'
|
assert table.template_params == '{"param": 1}'
|
||||||
assert table.normalize_columns is False
|
assert table.normalize_columns is False
|
||||||
|
|
||||||
self.items_to_delete = [table]
|
db.session.delete(table)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
with examples_db.get_sqla_engine() as engine:
|
with examples_db.get_sqla_engine() as engine:
|
||||||
engine.execute("DROP TABLE test_create_sqla_table_api")
|
engine.execute("DROP TABLE test_create_sqla_table_api")
|
||||||
|
|||||||
@@ -15,125 +15,78 @@
|
|||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from typing import Any, cast
|
from typing import cast
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from superset.commands.dataset.exceptions import (
|
from superset import db
|
||||||
DatabaseNotFoundValidationError,
|
from superset.commands.dataset.exceptions import DatasetInvalidError
|
||||||
DatasetExistsValidationError,
|
|
||||||
DatasetForbiddenError,
|
|
||||||
DatasetInvalidError,
|
|
||||||
DatasetNotFoundError,
|
|
||||||
MultiCatalogDisabledValidationError,
|
|
||||||
)
|
|
||||||
from superset.commands.dataset.update import UpdateDatasetCommand, validate_folders
|
from superset.commands.dataset.update import UpdateDatasetCommand, validate_folders
|
||||||
from superset.commands.exceptions import OwnersNotFoundValidationError
|
from superset.connectors.sqla.models import SqlaTable
|
||||||
from superset.datasets.schemas import FolderSchema
|
from superset.datasets.schemas import FolderSchema
|
||||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
from superset.models.core import Database
|
||||||
from superset.exceptions import SupersetSecurityException
|
|
||||||
from tests.unit_tests.conftest import with_feature_flags
|
from tests.unit_tests.conftest import with_feature_flags
|
||||||
|
|
||||||
|
|
||||||
def test_update_dataset_not_found(mocker: MockerFixture) -> None:
|
@pytest.mark.usefixture("session")
|
||||||
|
def test_update_uniqueness_error(mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
Test updating an unexisting ID raises a `DatasetNotFoundError`.
|
Test uniqueness validation in dataset update command.
|
||||||
"""
|
"""
|
||||||
mock_dataset_dao = mocker.patch("superset.commands.dataset.update.DatasetDAO")
|
SqlaTable.metadata.create_all(db.session.get_bind())
|
||||||
mock_dataset_dao.find_by_id.return_value = None
|
|
||||||
|
|
||||||
with pytest.raises(DatasetNotFoundError):
|
# First, make sure session is clean
|
||||||
UpdateDatasetCommand(1, {"name": "test"}).run()
|
db.session.rollback()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set up test data
|
||||||
|
database = Database(database_name="my_db", sqlalchemy_uri="sqlite://")
|
||||||
|
bar = SqlaTable(table_name="bar", schema="foo", database=database)
|
||||||
|
baz = SqlaTable(table_name="baz", schema="qux", database=database)
|
||||||
|
db.session.add_all([database, bar, baz])
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def test_update_dataset_forbidden(mocker: MockerFixture) -> None:
|
# Set up mocks
|
||||||
"""
|
mock_g = mocker.patch("superset.security.manager.g")
|
||||||
Test try updating a dataset without permission raises a `DatasetForbiddenError`.
|
mock_g.user = MagicMock()
|
||||||
"""
|
mocker.patch(
|
||||||
mock_dataset_dao = mocker.patch("superset.commands.dataset.update.DatasetDAO")
|
"superset.views.base.security_manager.can_access_all_datasources",
|
||||||
mock_dataset_dao.find_by_id.return_value = mocker.MagicMock()
|
return_value=True,
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
"superset.commands.dataset.update.security_manager.raise_for_ownership",
|
||||||
|
return_value=None,
|
||||||
|
)
|
||||||
|
mocker.patch.object(UpdateDatasetCommand, "compute_owners", return_value=[])
|
||||||
|
|
||||||
mocker.patch(
|
# Run the test that should fail
|
||||||
"superset.commands.dataset.update.security_manager.raise_for_ownership",
|
with pytest.raises(DatasetInvalidError):
|
||||||
side_effect=SupersetSecurityException(
|
UpdateDatasetCommand(
|
||||||
SupersetError(
|
bar.id,
|
||||||
error_type=SupersetErrorType.MISSING_OWNERSHIP_ERROR,
|
{
|
||||||
message="Sample message",
|
"table_name": "baz",
|
||||||
level=ErrorLevel.ERROR,
|
"schema": "qux",
|
||||||
|
},
|
||||||
|
).run()
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
# Clean up - this will run even if the test fails
|
||||||
|
try:
|
||||||
|
db.session.query(SqlaTable).filter(
|
||||||
|
SqlaTable.table_name.in_(["bar", "baz"]),
|
||||||
|
SqlaTable.schema.in_(["foo", "qux"]),
|
||||||
|
).delete(synchronize_session=False)
|
||||||
|
db.session.query(Database).filter(Database.database_name == "my_db").delete(
|
||||||
|
synchronize_session=False
|
||||||
)
|
)
|
||||||
),
|
db.session.commit()
|
||||||
)
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
with pytest.raises(DatasetForbiddenError):
|
|
||||||
UpdateDatasetCommand(1, {"name": "test"}).run()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("payload, exception, error_msg"),
|
|
||||||
[
|
|
||||||
(
|
|
||||||
{"database_id": 2},
|
|
||||||
DatabaseNotFoundValidationError,
|
|
||||||
"Database does not exist",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
{"catalog": "test"},
|
|
||||||
MultiCatalogDisabledValidationError,
|
|
||||||
"Only the default catalog is supported for this connection",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
{"table_name": "table", "schema": "schema"},
|
|
||||||
DatasetExistsValidationError,
|
|
||||||
"Dataset catalog.schema.table already exists",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
{"owners": [1]},
|
|
||||||
OwnersNotFoundValidationError,
|
|
||||||
"Owners are invalid",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_update_validation_errors(
|
|
||||||
payload: dict[str, Any],
|
|
||||||
exception: Exception,
|
|
||||||
error_msg: str,
|
|
||||||
mocker: MockerFixture,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Test validation errors for the `UpdateDatasetCommand`.
|
|
||||||
"""
|
|
||||||
mock_dataset_dao = mocker.patch("superset.commands.dataset.update.DatasetDAO")
|
|
||||||
mocker.patch(
|
|
||||||
"superset.commands.dataset.update.security_manager.raise_for_ownership",
|
|
||||||
)
|
|
||||||
mocker.patch("superset.commands.utils.security_manager.is_admin", return_value=True)
|
|
||||||
mocker.patch(
|
|
||||||
"superset.commands.utils.security_manager.get_user_by_id", return_value=None
|
|
||||||
)
|
|
||||||
mock_database = mocker.MagicMock()
|
|
||||||
mock_database.id = 1
|
|
||||||
mock_database.get_default_catalog.return_value = "catalog"
|
|
||||||
mock_database.allow_multi_catalog = False
|
|
||||||
mock_dataset = mocker.MagicMock()
|
|
||||||
mock_dataset.database = mock_database
|
|
||||||
mock_dataset.catalog = "catalog"
|
|
||||||
mock_dataset_dao.find_by_id.return_value = mock_dataset
|
|
||||||
|
|
||||||
if exception == DatabaseNotFoundValidationError:
|
|
||||||
mock_dataset_dao.get_database_by_id.return_value = None
|
|
||||||
else:
|
|
||||||
mock_dataset_dao.get_database_by_id.return_value = mock_database
|
|
||||||
|
|
||||||
if exception == DatasetExistsValidationError:
|
|
||||||
mock_dataset_dao.validate_update_uniqueness.return_value = False
|
|
||||||
else:
|
|
||||||
mock_dataset_dao.validate_update_uniqueness.return_value = True
|
|
||||||
|
|
||||||
with pytest.raises(DatasetInvalidError) as excinfo:
|
|
||||||
UpdateDatasetCommand(1, payload).run()
|
|
||||||
assert any(error_msg in str(exc) for exc in excinfo.value._exceptions)
|
|
||||||
|
|
||||||
|
|
||||||
@with_feature_flags(DATASET_FOLDERS=True)
|
@with_feature_flags(DATASET_FOLDERS=True)
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from pytest import fixture # noqa: PT013
|
from pytest import fixture # noqa: PT013
|
||||||
@@ -45,15 +45,9 @@ def connector_registry() -> Mock:
|
|||||||
return mock
|
return mock
|
||||||
|
|
||||||
|
|
||||||
def apply_max_row_limit(
|
def apply_max_row_limit(limit: int, max_limit: Optional[int] = None) -> int:
|
||||||
limit: int,
|
if max_limit is None:
|
||||||
server_pagination: bool | None = None,
|
max_limit = create_app_config()["SQL_MAX_ROW"]
|
||||||
) -> int:
|
|
||||||
max_limit = (
|
|
||||||
create_app_config()["TABLE_VIZ_MAX_ROW_SERVER"]
|
|
||||||
if server_pagination
|
|
||||||
else create_app_config()["SQL_MAX_ROW"]
|
|
||||||
)
|
|
||||||
if limit != 0:
|
if limit != 0:
|
||||||
return min(max_limit, limit)
|
return min(max_limit, limit)
|
||||||
return max_limit
|
return max_limit
|
||||||
|
|||||||
Reference in New Issue
Block a user