mirror of
https://github.com/apache/superset.git
synced 2026-05-02 06:24:37 +00:00
Compare commits
45 Commits
docs_opena
...
loader-exp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8013b32f0e | ||
|
|
adeed60fe0 | ||
|
|
546945e7a6 | ||
|
|
5b2f1bbf9e | ||
|
|
875f538d54 | ||
|
|
b7d3ff1e85 | ||
|
|
c03964dc5f | ||
|
|
950a3313d8 | ||
|
|
e2a22d481c | ||
|
|
b4e2406385 | ||
|
|
ca9e74edd8 | ||
|
|
39b3de6b5d | ||
|
|
26563bb330 | ||
|
|
0653e123cc | ||
|
|
76358ed64e | ||
|
|
217f11a8f7 | ||
|
|
af21ef2497 | ||
|
|
51c25831e8 | ||
|
|
be41e0526a | ||
|
|
0f240ea1b2 | ||
|
|
e520538af6 | ||
|
|
e03d840d06 | ||
|
|
1921ba993e | ||
|
|
b050897ebd | ||
|
|
0bdd8a223d | ||
|
|
d12f86363f | ||
|
|
9f680a63f8 | ||
|
|
928a052440 | ||
|
|
fbc84a1f9a | ||
|
|
fa1693dc5f | ||
|
|
8a8fb49617 | ||
|
|
dc4474889d | ||
|
|
29ac507d56 | ||
|
|
7f14e434c8 | ||
|
|
21ca26acd7 | ||
|
|
33e48146b0 | ||
|
|
73701b7295 | ||
|
|
22475e787e | ||
|
|
9e38a0cc29 | ||
|
|
a391ebecca | ||
|
|
72cd9dffa3 | ||
|
|
4ed05f4ff1 | ||
|
|
871cfe0c78 | ||
|
|
a928f8cd9e | ||
|
|
afaaf64f52 |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -27,6 +27,8 @@ updates:
|
|||||||
- package-ecosystem: "uv"
|
- package-ecosystem: "uv"
|
||||||
directory: "requirements/"
|
directory: "requirements/"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
labels:
|
labels:
|
||||||
- uv
|
- uv
|
||||||
- dependabot
|
- dependabot
|
||||||
|
|||||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@@ -48,6 +48,8 @@ jobs:
|
|||||||
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/applitools/core, pkg:npm/applitools/core-base, pkg:npm/applitools/css-tree, pkg:npm/applitools/ec-client, pkg:npm/applitools/eg-socks5-proxy-server, pkg:npm/applitools/eyes, pkg:npm/applitools/eyes-cypress, pkg:npm/applitools/nml-client, pkg:npm/applitools/tunnel-client, pkg:npm/applitools/utils, pkg:npm/node-forge@1.3.1, pkg:npm/rgbcolor, pkg:npm/jszip@3.10.1
|
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/applitools/core, pkg:npm/applitools/core-base, pkg:npm/applitools/css-tree, pkg:npm/applitools/ec-client, pkg:npm/applitools/eg-socks5-proxy-server, pkg:npm/applitools/eyes, pkg:npm/applitools/eyes-cypress, pkg:npm/applitools/nml-client, pkg:npm/applitools/tunnel-client, pkg:npm/applitools/utils, pkg:npm/node-forge@1.3.1, pkg:npm/rgbcolor, pkg:npm/jszip@3.10.1
|
||||||
|
|
||||||
python-dependency-liccheck:
|
python-dependency-liccheck:
|
||||||
|
# NOTE: Configuration for liccheck lives in our pyproject.yml.
|
||||||
|
# You cannot use a liccheck.ini file in this workflow.
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout Repository"
|
- name: "Checkout Repository"
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ Join our growing community!
|
|||||||
- [Cape Crypto](https://capecrypto.com)
|
- [Cape Crypto](https://capecrypto.com)
|
||||||
- [Capital Service S.A.](https://capitalservice.pl) [@pkonarzewski]
|
- [Capital Service S.A.](https://capitalservice.pl) [@pkonarzewski]
|
||||||
- [Clark.de](https://clark.de/)
|
- [Clark.de](https://clark.de/)
|
||||||
|
- [Europace](https://europace.de)
|
||||||
- [KarrotPay](https://www.daangnpay.com/)
|
- [KarrotPay](https://www.daangnpay.com/)
|
||||||
- [Remita](https://remita.net) [@mujibishola]
|
- [Remita](https://remita.net) [@mujibishola]
|
||||||
- [Taveo](https://www.taveo.com) [@codek]
|
- [Taveo](https://www.taveo.com) [@codek]
|
||||||
|
|||||||
@@ -302,6 +302,15 @@ 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`:
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,51 @@ check the [supersetbot docker](https://github.com/apache-superset/supersetbot)
|
|||||||
subcommand and the [docker.yml](https://github.com/apache/superset/blob/master/.github/workflows/docker.yml)
|
subcommand and the [docker.yml](https://github.com/apache/superset/blob/master/.github/workflows/docker.yml)
|
||||||
GitHub action.
|
GitHub action.
|
||||||
|
|
||||||
|
## Building your own production Docker image
|
||||||
|
|
||||||
|
Every Superset deployment will require its own set of drivers depending on the data warehouse(s),
|
||||||
|
etc. so we recommend that users build their own Docker image by extending the `lean` image.
|
||||||
|
|
||||||
|
Here's an example Dockerfile that does this. Follow the in-line comments to customize it for
|
||||||
|
your desired Superset version and database drivers. The comments also note that a certain feature flag will
|
||||||
|
have to be enabled in your config file.
|
||||||
|
|
||||||
|
You would build the image with `docker build -t mysuperset:latest .` or `docker build -t ourcompanysuperset:4.1.2 .`
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
# change this to apache/superset:4.1.2 or whatever version you want to build from;
|
||||||
|
# otherwise the default is the latest commit on GitHub master branch
|
||||||
|
FROM apache/superset:master
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# Set environment variable for Playwright
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/playwright-browsers
|
||||||
|
|
||||||
|
# Install packages using uv into the virtual environment
|
||||||
|
# Superset started using uv after the 4.1 branch; if you are building from apache/superset:4.1.x,
|
||||||
|
# replace the first two lines with RUN pip install \
|
||||||
|
RUN . /app/.venv/bin/activate && \
|
||||||
|
uv pip install \
|
||||||
|
# install psycopg2 for using PostgreSQL metadata store - could be a MySQL package if using that backend:
|
||||||
|
psycopg2-binary \
|
||||||
|
# add the driver(s) for your data warehouse(s), in this example we're showing for Microsoft SQL Server:
|
||||||
|
pymssql \
|
||||||
|
# package needed for using single-sign on authentication:
|
||||||
|
Authlib \
|
||||||
|
# install Playwright for taking screenshots for Alerts & Reports. This assumes the feature flag PLAYWRIGHT_REPORTS_AND_THUMBNAILS is enabled
|
||||||
|
# That feature flag will default to True starting in 6.0.0
|
||||||
|
# Playwright works only with Chrome.
|
||||||
|
# If you are still using Selenium instead of Playwright, you would instead install here the selenium package and a headless browser & webdriver
|
||||||
|
playwright \
|
||||||
|
&& PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/playwright-browsers playwright install chromium
|
||||||
|
|
||||||
|
# Switch back to the superset user
|
||||||
|
USER superset
|
||||||
|
|
||||||
|
CMD ["/app/docker/entrypoints/run-server.sh"]
|
||||||
|
```
|
||||||
|
|
||||||
## Key ARGs in Dockerfile
|
## Key ARGs in Dockerfile
|
||||||
|
|
||||||
- `BUILD_TRANSLATIONS`: whether to build the translations into the image. For the
|
- `BUILD_TRANSLATIONS`: whether to build the translations into the image. For the
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ You will need to back up your metadata DB. That could mean backing up the servic
|
|||||||
|
|
||||||
You will also need to extend the Superset docker image. The default `lean` images do not contain drivers needed to access your metadata database (Postgres or MySQL), nor to access your data warehouse, nor the headless browser needed for Alerts & Reports. You could run a `-dev` image while demoing Superset, which has some of this, but you'll still need to install the driver for your data warehouse. The `-dev` images run as root, which is not recommended for production.
|
You will also need to extend the Superset docker image. The default `lean` images do not contain drivers needed to access your metadata database (Postgres or MySQL), nor to access your data warehouse, nor the headless browser needed for Alerts & Reports. You could run a `-dev` image while demoing Superset, which has some of this, but you'll still need to install the driver for your data warehouse. The `-dev` images run as root, which is not recommended for production.
|
||||||
|
|
||||||
Ideally you will build your own image of Superset that extends `lean`, adding what your deployment needs.
|
Ideally you will build your own image of Superset that extends `lean`, adding what your deployment needs. See [Building your own production Docker image](/docs/installation/docker-builds/#building-your-own-production-docker-image).
|
||||||
|
|
||||||
See [Docker Build Presets](/docs/installation/docker-builds/#build-presets) for more information about the different image versions you can extend.
|
|
||||||
|
|
||||||
## [Kubernetes (K8s)](/docs/installation/kubernetes.mdx)
|
## [Kubernetes (K8s)](/docs/installation/kubernetes.mdx)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
title: CVEs fixed by release
|
title: CVEs fixed by release
|
||||||
sidebar_position: 2
|
sidebar_position: 2
|
||||||
---
|
---
|
||||||
|
#### Version 4.1.2
|
||||||
|
|
||||||
|
| CVE | Title | Affected |
|
||||||
|
|:---------------|:-----------------------------------------------------------------------------------|---------:|
|
||||||
|
| CVE-2025-27696 | Improper authorization leading to resource ownership takeover | < 4.1.2 |
|
||||||
|
|
||||||
#### Version 4.1.0
|
#### Version 4.1.0
|
||||||
|
|
||||||
| CVE | Title | Affected |
|
| CVE | Title | Affected |
|
||||||
|
|||||||
@@ -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.5",
|
"antd": "^5.25.1",
|
||||||
"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",
|
||||||
@@ -44,12 +44,12 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
"eslint": "^8.0.0",
|
"eslint": "^8.0.0",
|
||||||
"eslint-config-prettier": "^10.1.2",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"prettier": "^2.0.0",
|
"prettier": "^2.0.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"webpack": "^5.99.7"
|
"webpack": "^5.99.8"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|||||||
148
docs/yarn.lock
148
docs/yarn.lock
@@ -1092,20 +1092,13 @@
|
|||||||
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.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.25.9", "@babel/runtime@^7.8.4":
|
"@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":
|
||||||
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"
|
||||||
@@ -4186,10 +4179,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.5:
|
antd@^5.25.1:
|
||||||
version "5.24.5"
|
version "5.25.1"
|
||||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.24.5.tgz#b0976a113163888d1477f9e666c3c23352b098e9"
|
resolved "https://registry.yarnpkg.com/antd/-/antd-5.25.1.tgz#859b419a18d113492304ccd66c29074a71902241"
|
||||||
integrity sha512-1lAv/G+9ewQanyoAo3JumQmIlVxwo5QwWGb6QCHYc40Cq0NxC/EzITcjsgq1PSaTUpLkKq8A2l7Fjtu47vqQBg==
|
integrity sha512-4KC7KuPCjr0z3Vuw9DsF+ceqJaPLbuUI3lOX1sY8ix25ceamp+P8yxOmk3Y2JHCD2ZAhq+5IQ/DTJRN2adWYKQ==
|
||||||
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,37 +4199,37 @@ antd@^5.24.5:
|
|||||||
classnames "^2.5.1"
|
classnames "^2.5.1"
|
||||||
copy-to-clipboard "^3.3.3"
|
copy-to-clipboard "^3.3.3"
|
||||||
dayjs "^1.11.11"
|
dayjs "^1.11.11"
|
||||||
rc-cascader "~3.33.1"
|
rc-cascader "~3.34.0"
|
||||||
rc-checkbox "~3.5.0"
|
rc-checkbox "~3.5.0"
|
||||||
rc-collapse "~3.9.0"
|
rc-collapse "~3.9.0"
|
||||||
rc-dialog "~9.6.0"
|
rc-dialog "~9.6.0"
|
||||||
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.11.1"
|
rc-image "~7.12.0"
|
||||||
rc-input "~1.7.3"
|
rc-input "~1.8.0"
|
||||||
rc-input-number "~9.4.0"
|
rc-input-number "~9.5.0"
|
||||||
rc-mentions "~2.19.1"
|
rc-mentions "~2.20.0"
|
||||||
rc-menu "~9.16.1"
|
rc-menu "~9.16.1"
|
||||||
rc-motion "^2.9.5"
|
rc-motion "^2.9.5"
|
||||||
rc-notification "~5.6.3"
|
rc-notification "~5.6.4"
|
||||||
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"
|
||||||
rc-rate "~2.13.1"
|
rc-rate "~2.13.1"
|
||||||
rc-resize-observer "^1.4.3"
|
rc-resize-observer "^1.4.3"
|
||||||
rc-segmented "~2.7.0"
|
rc-segmented "~2.7.0"
|
||||||
rc-select "~14.16.6"
|
rc-select "~14.16.7"
|
||||||
rc-slider "~11.1.8"
|
rc-slider "~11.1.8"
|
||||||
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.5.1"
|
rc-tabs "~15.6.1"
|
||||||
rc-textarea "~1.9.0"
|
rc-textarea "~1.10.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"
|
||||||
rc-upload "~4.8.1"
|
rc-upload "~4.9.0"
|
||||||
rc-util "^5.44.4"
|
rc-util "^5.44.4"
|
||||||
scroll-into-view-if-needed "^3.1.0"
|
scroll-into-view-if-needed "^3.1.0"
|
||||||
throttle-debounce "^5.0.2"
|
throttle-debounce "^5.0.2"
|
||||||
@@ -5674,12 +5667,7 @@ 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.11, dayjs@^1.11.13:
|
||||||
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==
|
||||||
@@ -6259,10 +6247,10 @@ escape-string-regexp@^5.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
|
||||||
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
|
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
|
||||||
|
|
||||||
eslint-config-prettier@^10.1.2:
|
eslint-config-prettier@^10.1.5:
|
||||||
version "10.1.2"
|
version "10.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz#31a4b393c40c4180202c27e829af43323bf85276"
|
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
|
||||||
integrity sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==
|
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
|
||||||
|
|
||||||
eslint-plugin-prettier@^4.0.0:
|
eslint-plugin-prettier@^4.0.0:
|
||||||
version "4.2.1"
|
version "4.2.1"
|
||||||
@@ -10617,10 +10605,10 @@ raw-body@2.5.2:
|
|||||||
iconv-lite "0.4.24"
|
iconv-lite "0.4.24"
|
||||||
unpipe "1.0.0"
|
unpipe "1.0.0"
|
||||||
|
|
||||||
rc-cascader@~3.33.1:
|
rc-cascader@~3.34.0:
|
||||||
version "3.33.1"
|
version "3.34.0"
|
||||||
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.33.1.tgz#19e01462ef5ef51b723c1f562c7b9cde4691e7ee"
|
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.34.0.tgz#56f936ab6b1229bab7d558701ce9b9e96536582c"
|
||||||
integrity sha512-Kyl4EJ7ZfCBuidmZVieegcbFw0RcU5bHHSbtEdmuLYd0fYHCAiYKZ6zon7fWAVyC6rWWOOib0XKdTSf7ElC9rg==
|
integrity sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.25.7"
|
"@babel/runtime" "^7.25.7"
|
||||||
classnames "^2.3.1"
|
classnames "^2.3.1"
|
||||||
@@ -10688,10 +10676,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.11.1:
|
rc-image@~7.12.0:
|
||||||
version "7.11.1"
|
version "7.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-7.11.1.tgz#3ab290708dc053d3681de94186522e4e594f6772"
|
resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-7.12.0.tgz#95e9314701e668217d113c1f29b4f01ac025cafe"
|
||||||
integrity sha512-XuoWx4KUXg7hNy5mRTy1i8c8p3K8boWg6UajbHpDXS5AlRVucNfTi5YxTtPBTBzegxAZpvuLfh3emXFt6ybUdA==
|
integrity sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.11.2"
|
"@babel/runtime" "^7.11.2"
|
||||||
"@rc-component/portal" "^1.0.2"
|
"@rc-component/portal" "^1.0.2"
|
||||||
@@ -10700,37 +10688,37 @@ rc-image@~7.11.1:
|
|||||||
rc-motion "^2.6.2"
|
rc-motion "^2.6.2"
|
||||||
rc-util "^5.34.1"
|
rc-util "^5.34.1"
|
||||||
|
|
||||||
rc-input-number@~9.4.0:
|
rc-input-number@~9.5.0:
|
||||||
version "9.4.0"
|
version "9.5.0"
|
||||||
resolved "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.4.0.tgz"
|
resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-9.5.0.tgz#b47963d0f2cbd85ab2f1badfdc089a904c073f38"
|
||||||
integrity sha512-Tiy4DcXcFXAf9wDhN8aUAyMeCLHJUHA/VA/t7Hj8ZEx5ETvxG7MArDOSE6psbiSCo+vJPm4E3fGN710ITVn6GA==
|
integrity sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==
|
||||||
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.7.1"
|
rc-input "~1.8.0"
|
||||||
rc-util "^5.40.1"
|
rc-util "^5.40.1"
|
||||||
|
|
||||||
rc-input@~1.7.1, rc-input@~1.7.3:
|
rc-input@~1.8.0:
|
||||||
version "1.7.3"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-1.7.3.tgz#cb334a17b93ce985bceb243b4c111a5ed641e0e3"
|
resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-1.8.0.tgz#d2f4404befebf2fbdc28390d5494c302f74ae974"
|
||||||
integrity sha512-A5w4egJq8+4JzlQ55FfQjDnPvOaAbzwC3VLOAdOytyek3TboSOP9qxN+Gifup+shVXfvecBLBbWBpWxmk02SWQ==
|
integrity sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==
|
||||||
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.19.1:
|
rc-mentions@~2.20.0:
|
||||||
version "2.19.1"
|
version "2.20.0"
|
||||||
resolved "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.19.1.tgz"
|
resolved "https://registry.yarnpkg.com/rc-mentions/-/rc-mentions-2.20.0.tgz#3bbeac0352b02e0ce3e1244adb48701bb6903bf7"
|
||||||
integrity sha512-KK3bAc/bPFI993J3necmaMXD2reZTzytZdlTvkeBbp50IGH1BDPDvxLdHDUrpQx2b2TGaVJsn+86BvYa03kGqA==
|
integrity sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==
|
||||||
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.7.1"
|
rc-input "~1.8.0"
|
||||||
rc-menu "~9.16.0"
|
rc-menu "~9.16.0"
|
||||||
rc-textarea "~1.9.0"
|
rc-textarea "~1.10.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:
|
||||||
@@ -10754,10 +10742,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.3:
|
rc-notification@~5.6.4:
|
||||||
version "5.6.3"
|
version "5.6.4"
|
||||||
resolved "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.3.tgz"
|
resolved "https://registry.yarnpkg.com/rc-notification/-/rc-notification-5.6.4.tgz#ea89c39c13cd517fdfd97fe63f03376fabb78544"
|
||||||
integrity sha512-42szwnn8VYQoT6GnjO00i1iwqV9D1TTMvxObWsuLwgl0TsOokzhkYiufdtQBsJMFjJravS1hfDKVMHLKLcPE4g==
|
integrity sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.10.1"
|
"@babel/runtime" "^7.10.1"
|
||||||
classnames "2.x"
|
classnames "2.x"
|
||||||
@@ -10833,10 +10821,10 @@ rc-segmented@~2.7.0:
|
|||||||
rc-motion "^2.4.4"
|
rc-motion "^2.4.4"
|
||||||
rc-util "^5.17.0"
|
rc-util "^5.17.0"
|
||||||
|
|
||||||
rc-select@~14.16.2, rc-select@~14.16.6:
|
rc-select@~14.16.2, rc-select@~14.16.7:
|
||||||
version "14.16.6"
|
version "14.16.8"
|
||||||
resolved "https://registry.npmjs.org/rc-select/-/rc-select-14.16.6.tgz"
|
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.16.8.tgz#78e6782f1ccc1f03d9003bc3effa4ed609d29a97"
|
||||||
integrity sha512-YPMtRPqfZWOm2XGTbx5/YVr1HT0vn//8QS77At0Gjb3Lv+Lbut0IORJPKLWu1hQ3u4GsA0SrDzs7nI8JG7Zmyg==
|
integrity sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.10.1"
|
"@babel/runtime" "^7.10.1"
|
||||||
"@rc-component/trigger" "^2.1.1"
|
"@rc-component/trigger" "^2.1.1"
|
||||||
@@ -10885,10 +10873,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.5.1:
|
rc-tabs@~15.6.1:
|
||||||
version "15.5.1"
|
version "15.6.1"
|
||||||
resolved "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.1.tgz"
|
resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-15.6.1.tgz#f0b6c65384dfa09a64eb539e86a0667c7a650708"
|
||||||
integrity sha512-yiWivLAjEo5d1v2xlseB2dQocsOhkoVSfo1krS8v8r+02K+TBUjSjXIf7dgyVSxp6wRIPv5pMi5hanNUlQMgUA==
|
integrity sha512-/HzDV1VqOsUWyuC0c6AkxVYFjvx9+rFPKZ32ejxX0Uc7QCzcEjTA9/xMgv4HemPKwzBNX8KhGVbbumDjnj92aA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.11.2"
|
"@babel/runtime" "^7.11.2"
|
||||||
classnames "2.x"
|
classnames "2.x"
|
||||||
@@ -10898,14 +10886,14 @@ rc-tabs@~15.5.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.9.0:
|
rc-textarea@~1.10.0:
|
||||||
version "1.9.0"
|
version "1.10.0"
|
||||||
resolved "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.9.0.tgz"
|
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.0.tgz#f8f962ef83be0b8e35db97cf03dbfb86ddd9c46c"
|
||||||
integrity sha512-dQW/Bc/MriPBTugj2Kx9PMS5eXCCGn2cxoIaichjbNvOiARlaHdI99j4DTxLl/V8+PIfW06uFy7kjfUIDDKyxQ==
|
integrity sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.10.1"
|
"@babel/runtime" "^7.10.1"
|
||||||
classnames "^2.2.1"
|
classnames "^2.2.1"
|
||||||
rc-input "~1.7.1"
|
rc-input "~1.8.0"
|
||||||
rc-resize-observer "^1.0.0"
|
rc-resize-observer "^1.0.0"
|
||||||
rc-util "^5.27.0"
|
rc-util "^5.27.0"
|
||||||
|
|
||||||
@@ -10941,10 +10929,10 @@ rc-tree@~5.13.0, rc-tree@~5.13.1:
|
|||||||
rc-util "^5.16.1"
|
rc-util "^5.16.1"
|
||||||
rc-virtual-list "^3.5.1"
|
rc-virtual-list "^3.5.1"
|
||||||
|
|
||||||
rc-upload@~4.8.1:
|
rc-upload@~4.9.0:
|
||||||
version "4.8.1"
|
version "4.9.0"
|
||||||
resolved "https://registry.npmjs.org/rc-upload/-/rc-upload-4.8.1.tgz"
|
resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.9.0.tgz#911963ab5a0b538c743765371c05e2de9e3f5436"
|
||||||
integrity sha512-toEAhwl4hjLAI1u8/CgKWt30BR06ulPa4iGQSMvSXoHzO88gPCslxqV/mnn4gJU7PDoltGIC9Eh+wkeudqgHyw==
|
integrity sha512-pAzlPnyiFn1GCtEybEG2m9nXNzQyWXqWV2xFYCmDxjN9HzyjS5Pz2F+pbNdYw8mMJsixLEKLG0wVy9vOGxJMJA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.18.3"
|
"@babel/runtime" "^7.18.3"
|
||||||
classnames "^2.2.5"
|
classnames "^2.2.5"
|
||||||
@@ -13045,10 +13033,10 @@ webpack-sources@^3.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||||
|
|
||||||
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.7:
|
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.8:
|
||||||
version "5.99.7"
|
version "5.99.8"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.7.tgz#60201c1ca66da046b07d006c2f6e0cc5e8a7bdba"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.8.tgz#dd31a020b7c092d30c4c6d9a4edb95809e7f5946"
|
||||||
integrity sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==
|
integrity sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope" "^3.7.7"
|
"@types/eslint-scope" "^3.7.7"
|
||||||
"@types/estree" "^1.0.6"
|
"@types/estree" "^1.0.6"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ dependencies = [
|
|||||||
"cryptography>=42.0.4, <45.0.0",
|
"cryptography>=42.0.4, <45.0.0",
|
||||||
"deprecation>=2.1.0, <2.2.0",
|
"deprecation>=2.1.0, <2.2.0",
|
||||||
"flask>=2.2.5, <3.0.0",
|
"flask>=2.2.5, <3.0.0",
|
||||||
"flask-appbuilder>=4.6.3, <5.0.0",
|
"flask-appbuilder>=4.6.4, <5.0.0",
|
||||||
"flask-caching>=2.1.0, <3",
|
"flask-caching>=2.1.0, <3",
|
||||||
"flask-compress>=1.13, <2.0",
|
"flask-compress>=1.13, <2.0",
|
||||||
"flask-talisman>=1.0.0, <2.0",
|
"flask-talisman>=1.0.0, <2.0",
|
||||||
@@ -240,6 +240,12 @@ disallow_untyped_calls = false
|
|||||||
disallow_untyped_defs = false
|
disallow_untyped_defs = false
|
||||||
disable_error_code = "annotation-unchecked"
|
disable_error_code = "annotation-unchecked"
|
||||||
|
|
||||||
|
# TODO: remove this once cryptography is fixed, introduced in cryptography 44.0.3
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "cryptography.*"
|
||||||
|
ignore_errors = true
|
||||||
|
follow_imports = "skip"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
# Exclude a variety of commonly ignored directories.
|
# Exclude a variety of commonly ignored directories.
|
||||||
exclude = [
|
exclude = [
|
||||||
@@ -272,7 +278,6 @@ exclude = [
|
|||||||
"venv",
|
"venv",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Same as Black.
|
# Same as Black.
|
||||||
line-length = 88
|
line-length = 88
|
||||||
indent-width = 4
|
indent-width = 4
|
||||||
@@ -367,6 +372,7 @@ docstring-code-line-length = "dynamic"
|
|||||||
requirement_txt_file = "requirements/base.txt"
|
requirement_txt_file = "requirements/base.txt"
|
||||||
authorized_licenses = [
|
authorized_licenses = [
|
||||||
"academic free license (afl)",
|
"academic free license (afl)",
|
||||||
|
"any-osi",
|
||||||
"apache license 2.0",
|
"apache license 2.0",
|
||||||
"apache software",
|
"apache software",
|
||||||
"apache software, bsd",
|
"apache software, bsd",
|
||||||
@@ -380,6 +386,7 @@ authorized_licenses = [
|
|||||||
"osi approved",
|
"osi approved",
|
||||||
"psf-2.0",
|
"psf-2.0",
|
||||||
"python software foundation",
|
"python software foundation",
|
||||||
|
"simplified bsd",
|
||||||
"the unlicense (unlicense)",
|
"the unlicense (unlicense)",
|
||||||
"the unlicense",
|
"the unlicense",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile pyproject.toml requirements/base.in -o requirements/base.txt
|
# uv pip compile pyproject.toml requirements/base.in -o requirements/base.txt
|
||||||
alembic==1.15.1
|
alembic==1.15.2
|
||||||
# via flask-migrate
|
# via flask-migrate
|
||||||
amqp==5.3.1
|
amqp==5.3.1
|
||||||
# via kombu
|
# via kombu
|
||||||
@@ -8,7 +8,7 @@ apispec==6.6.1
|
|||||||
# via
|
# via
|
||||||
# -r requirements/base.in
|
# -r requirements/base.in
|
||||||
# flask-appbuilder
|
# flask-appbuilder
|
||||||
apsw==3.49.1.0
|
apsw==3.49.2.0
|
||||||
# via shillelagh
|
# via shillelagh
|
||||||
async-timeout==4.0.3
|
async-timeout==4.0.3
|
||||||
# via
|
# via
|
||||||
@@ -32,7 +32,7 @@ billiard==4.2.1
|
|||||||
# via celery
|
# via celery
|
||||||
blinker==1.9.0
|
blinker==1.9.0
|
||||||
# via flask
|
# via flask
|
||||||
bottleneck==1.4.2
|
bottleneck==1.5.0
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
brotli==1.1.0
|
brotli==1.1.0
|
||||||
# via flask-compress
|
# via flask-compress
|
||||||
@@ -42,11 +42,11 @@ cachelib==0.13.0
|
|||||||
# flask-session
|
# flask-session
|
||||||
cachetools==5.5.2
|
cachetools==5.5.2
|
||||||
# via google-auth
|
# via google-auth
|
||||||
cattrs==24.1.2
|
cattrs==24.1.3
|
||||||
# via requests-cache
|
# via requests-cache
|
||||||
celery==5.5.2
|
celery==5.5.2
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
certifi==2025.1.31
|
certifi==2025.4.26
|
||||||
# via
|
# via
|
||||||
# requests
|
# requests
|
||||||
# selenium
|
# selenium
|
||||||
@@ -54,9 +54,9 @@ cffi==1.17.1
|
|||||||
# via
|
# via
|
||||||
# cryptography
|
# cryptography
|
||||||
# pynacl
|
# pynacl
|
||||||
charset-normalizer==3.4.1
|
charset-normalizer==3.4.2
|
||||||
# via requests
|
# via requests
|
||||||
click==8.1.8
|
click==8.2.0
|
||||||
# via
|
# via
|
||||||
# apache-superset (pyproject.toml)
|
# apache-superset (pyproject.toml)
|
||||||
# celery
|
# celery
|
||||||
@@ -99,7 +99,7 @@ email-validator==2.2.0
|
|||||||
# via flask-appbuilder
|
# via flask-appbuilder
|
||||||
et-xmlfile==2.0.0
|
et-xmlfile==2.0.0
|
||||||
# via openpyxl
|
# via openpyxl
|
||||||
exceptiongroup==1.2.2
|
exceptiongroup==1.3.0
|
||||||
# via
|
# via
|
||||||
# cattrs
|
# cattrs
|
||||||
# trio
|
# trio
|
||||||
@@ -118,7 +118,7 @@ flask==2.3.3
|
|||||||
# flask-session
|
# flask-session
|
||||||
# flask-sqlalchemy
|
# flask-sqlalchemy
|
||||||
# flask-wtf
|
# flask-wtf
|
||||||
flask-appbuilder==4.6.3
|
flask-appbuilder==4.6.4
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
flask-babel==2.0.0
|
flask-babel==2.0.0
|
||||||
# via flask-appbuilder
|
# via flask-appbuilder
|
||||||
@@ -152,13 +152,12 @@ geographiclib==2.0
|
|||||||
# via geopy
|
# via geopy
|
||||||
geopy==2.4.1
|
geopy==2.4.1
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
google-auth==2.38.0
|
google-auth==2.40.1
|
||||||
# via shillelagh
|
# via shillelagh
|
||||||
greenlet==3.1.1
|
greenlet==3.1.1
|
||||||
# via
|
# via
|
||||||
# apache-superset (pyproject.toml)
|
# apache-superset (pyproject.toml)
|
||||||
# shillelagh
|
# shillelagh
|
||||||
# sqlalchemy
|
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
@@ -174,6 +173,7 @@ idna==3.10
|
|||||||
# email-validator
|
# email-validator
|
||||||
# requests
|
# requests
|
||||||
# trio
|
# trio
|
||||||
|
# url-normalize
|
||||||
importlib-metadata==8.7.0
|
importlib-metadata==8.7.0
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
isodate==0.7.2
|
isodate==0.7.2
|
||||||
@@ -190,7 +190,7 @@ jsonpath-ng==1.7.0
|
|||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
jsonschema==4.23.0
|
jsonschema==4.23.0
|
||||||
# via flask-appbuilder
|
# via flask-appbuilder
|
||||||
jsonschema-specifications==2024.10.1
|
jsonschema-specifications==2025.4.1
|
||||||
# via jsonschema
|
# via jsonschema
|
||||||
kombu==5.5.3
|
kombu==5.5.3
|
||||||
# via celery
|
# via celery
|
||||||
@@ -243,7 +243,9 @@ openpyxl==3.1.5
|
|||||||
ordered-set==4.1.0
|
ordered-set==4.1.0
|
||||||
# via flask-limiter
|
# via flask-limiter
|
||||||
outcome==1.3.0.post0
|
outcome==1.3.0.post0
|
||||||
# via trio
|
# via
|
||||||
|
# trio
|
||||||
|
# trio-websocket
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
# via
|
# via
|
||||||
# apache-superset (pyproject.toml)
|
# apache-superset (pyproject.toml)
|
||||||
@@ -263,7 +265,7 @@ parsedatetime==2.6
|
|||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
pgsanity==0.2.9
|
pgsanity==0.2.9
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
platformdirs==4.3.7
|
platformdirs==4.3.8
|
||||||
# via requests-cache
|
# via requests-cache
|
||||||
ply==3.11
|
ply==3.11
|
||||||
# via jsonpath-ng
|
# via jsonpath-ng
|
||||||
@@ -279,7 +281,7 @@ pyasn1==0.6.1
|
|||||||
# via
|
# via
|
||||||
# pyasn1-modules
|
# pyasn1-modules
|
||||||
# rsa
|
# rsa
|
||||||
pyasn1-modules==0.4.1
|
pyasn1-modules==0.4.2
|
||||||
# via google-auth
|
# via google-auth
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
# via cffi
|
# via cffi
|
||||||
@@ -336,13 +338,13 @@ requests-cache==1.2.1
|
|||||||
# via shillelagh
|
# via shillelagh
|
||||||
rich==13.9.4
|
rich==13.9.4
|
||||||
# via flask-limiter
|
# via flask-limiter
|
||||||
rpds-py==0.23.1
|
rpds-py==0.25.0
|
||||||
# via
|
# via
|
||||||
# jsonschema
|
# jsonschema
|
||||||
# referencing
|
# referencing
|
||||||
rsa==4.9
|
rsa==4.9.1
|
||||||
# via google-auth
|
# via google-auth
|
||||||
selenium==4.27.1
|
selenium==4.32.0
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
shillelagh==1.3.5
|
shillelagh==1.3.5
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
@@ -352,7 +354,6 @@ six==1.17.0
|
|||||||
# via
|
# via
|
||||||
# prison
|
# prison
|
||||||
# python-dateutil
|
# python-dateutil
|
||||||
# url-normalize
|
|
||||||
# wtforms-json
|
# wtforms-json
|
||||||
slack-sdk==3.35.0
|
slack-sdk==3.35.0
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
@@ -373,7 +374,7 @@ sqlalchemy-utils==0.38.3
|
|||||||
# via
|
# via
|
||||||
# apache-superset (pyproject.toml)
|
# apache-superset (pyproject.toml)
|
||||||
# flask-appbuilder
|
# flask-appbuilder
|
||||||
sqlglot==26.16.4
|
sqlglot==26.17.1
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
sqlparse==0.5.3
|
sqlparse==0.5.3
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
@@ -381,17 +382,18 @@ sshtunnel==0.4.0
|
|||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
tabulate==0.8.10
|
tabulate==0.8.10
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
trio==0.28.0
|
trio==0.30.0
|
||||||
# via
|
# via
|
||||||
# selenium
|
# selenium
|
||||||
# trio-websocket
|
# trio-websocket
|
||||||
trio-websocket==0.11.1
|
trio-websocket==0.12.2
|
||||||
# via selenium
|
# via selenium
|
||||||
typing-extensions==4.12.2
|
typing-extensions==4.13.2
|
||||||
# via
|
# via
|
||||||
# apache-superset (pyproject.toml)
|
# apache-superset (pyproject.toml)
|
||||||
# alembic
|
# alembic
|
||||||
# cattrs
|
# cattrs
|
||||||
|
# exceptiongroup
|
||||||
# limits
|
# limits
|
||||||
# pyopenssl
|
# pyopenssl
|
||||||
# referencing
|
# referencing
|
||||||
@@ -402,7 +404,7 @@ tzdata==2025.2
|
|||||||
# via
|
# via
|
||||||
# kombu
|
# kombu
|
||||||
# pandas
|
# pandas
|
||||||
url-normalize==1.4.3
|
url-normalize==2.2.1
|
||||||
# via requests-cache
|
# via requests-cache
|
||||||
urllib3==1.26.20
|
urllib3==1.26.20
|
||||||
# via
|
# via
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# uv pip compile requirements/development.in -c requirements/base.txt -o requirements/development.txt
|
# uv pip compile requirements/development.in -c requirements/base.txt -o requirements/development.txt
|
||||||
-e .
|
-e .
|
||||||
# via -r requirements/development.in
|
# via -r requirements/development.in
|
||||||
alembic==1.15.1
|
alembic==1.15.2
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# flask-migrate
|
# flask-migrate
|
||||||
@@ -14,7 +14,7 @@ apispec==6.6.1
|
|||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# flask-appbuilder
|
# flask-appbuilder
|
||||||
apsw==3.49.1.0
|
apsw==3.49.2.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# shillelagh
|
# shillelagh
|
||||||
@@ -51,7 +51,7 @@ blinker==1.9.0
|
|||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# flask
|
# flask
|
||||||
bottleneck==1.4.2
|
bottleneck==1.5.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
@@ -68,7 +68,7 @@ cachetools==5.5.2
|
|||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# google-auth
|
# google-auth
|
||||||
cattrs==24.1.2
|
cattrs==24.1.3
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# requests-cache
|
# requests-cache
|
||||||
@@ -76,7 +76,7 @@ celery==5.5.2
|
|||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
certifi==2025.1.31
|
certifi==2025.4.26
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# requests
|
# requests
|
||||||
@@ -88,11 +88,11 @@ cffi==1.17.1
|
|||||||
# pynacl
|
# pynacl
|
||||||
cfgv==3.4.0
|
cfgv==3.4.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
charset-normalizer==3.4.1
|
charset-normalizer==3.4.2
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# requests
|
# requests
|
||||||
click==8.1.8
|
click==8.2.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
@@ -176,7 +176,7 @@ et-xmlfile==2.0.0
|
|||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# openpyxl
|
# openpyxl
|
||||||
exceptiongroup==1.2.2
|
exceptiongroup==1.3.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# cattrs
|
# cattrs
|
||||||
@@ -202,7 +202,7 @@ flask==2.3.3
|
|||||||
# flask-sqlalchemy
|
# flask-sqlalchemy
|
||||||
# flask-testing
|
# flask-testing
|
||||||
# flask-wtf
|
# flask-wtf
|
||||||
flask-appbuilder==4.6.3
|
flask-appbuilder==4.6.4
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
@@ -280,7 +280,7 @@ google-api-core==2.23.0
|
|||||||
# google-cloud-core
|
# google-cloud-core
|
||||||
# pandas-gbq
|
# pandas-gbq
|
||||||
# sqlalchemy-bigquery
|
# sqlalchemy-bigquery
|
||||||
google-auth==2.38.0
|
google-auth==2.40.1
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# google-api-core
|
# google-api-core
|
||||||
@@ -318,7 +318,6 @@ greenlet==3.1.1
|
|||||||
# apache-superset
|
# apache-superset
|
||||||
# gevent
|
# gevent
|
||||||
# shillelagh
|
# shillelagh
|
||||||
# sqlalchemy
|
|
||||||
grpcio==1.71.0
|
grpcio==1.71.0
|
||||||
# via
|
# via
|
||||||
# apache-superset
|
# apache-superset
|
||||||
@@ -355,6 +354,7 @@ idna==3.10
|
|||||||
# email-validator
|
# email-validator
|
||||||
# requests
|
# requests
|
||||||
# trio
|
# trio
|
||||||
|
# url-normalize
|
||||||
importlib-metadata==8.7.0
|
importlib-metadata==8.7.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
@@ -389,7 +389,7 @@ jsonschema==4.23.0
|
|||||||
# openapi-spec-validator
|
# openapi-spec-validator
|
||||||
jsonschema-path==0.3.4
|
jsonschema-path==0.3.4
|
||||||
# via openapi-spec-validator
|
# via openapi-spec-validator
|
||||||
jsonschema-specifications==2024.10.1
|
jsonschema-specifications==2025.4.1
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# jsonschema
|
# jsonschema
|
||||||
@@ -495,6 +495,7 @@ outcome==1.3.0.post0
|
|||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# trio
|
# trio
|
||||||
|
# trio-websocket
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
@@ -542,7 +543,7 @@ pillow==10.3.0
|
|||||||
# via
|
# via
|
||||||
# apache-superset
|
# apache-superset
|
||||||
# matplotlib
|
# matplotlib
|
||||||
platformdirs==4.3.7
|
platformdirs==4.3.8
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# requests-cache
|
# requests-cache
|
||||||
@@ -598,7 +599,7 @@ pyasn1==0.6.1
|
|||||||
# pyasn1-modules
|
# pyasn1-modules
|
||||||
# python-ldap
|
# python-ldap
|
||||||
# rsa
|
# rsa
|
||||||
pyasn1-modules==0.4.1
|
pyasn1-modules==0.4.2
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# google-auth
|
# google-auth
|
||||||
@@ -731,22 +732,22 @@ rich==13.9.4
|
|||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# flask-limiter
|
# flask-limiter
|
||||||
rpds-py==0.23.1
|
rpds-py==0.25.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# jsonschema
|
# jsonschema
|
||||||
# referencing
|
# referencing
|
||||||
rsa==4.9
|
rsa==4.9.1
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# google-auth
|
# google-auth
|
||||||
ruff==0.8.0
|
ruff==0.8.0
|
||||||
# via apache-superset
|
# via apache-superset
|
||||||
selenium==4.27.1
|
selenium==4.32.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
setuptools==75.6.0
|
setuptools==80.7.1
|
||||||
# via
|
# via
|
||||||
# nodeenv
|
# nodeenv
|
||||||
# pandas-gbq
|
# pandas-gbq
|
||||||
@@ -767,7 +768,6 @@ six==1.17.0
|
|||||||
# prison
|
# prison
|
||||||
# python-dateutil
|
# python-dateutil
|
||||||
# rfc3339-validator
|
# rfc3339-validator
|
||||||
# url-normalize
|
|
||||||
# wtforms-json
|
# wtforms-json
|
||||||
slack-sdk==3.35.0
|
slack-sdk==3.35.0
|
||||||
# via
|
# via
|
||||||
@@ -799,7 +799,7 @@ sqlalchemy-utils==0.38.3
|
|||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
# flask-appbuilder
|
# flask-appbuilder
|
||||||
sqlglot==26.16.4
|
sqlglot==26.17.1
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
@@ -829,21 +829,22 @@ tqdm==4.67.1
|
|||||||
# prophet
|
# prophet
|
||||||
trino==0.330.0
|
trino==0.330.0
|
||||||
# via apache-superset
|
# via apache-superset
|
||||||
trio==0.28.0
|
trio==0.30.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# selenium
|
# selenium
|
||||||
# trio-websocket
|
# trio-websocket
|
||||||
trio-websocket==0.11.1
|
trio-websocket==0.12.2
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# selenium
|
# selenium
|
||||||
typing-extensions==4.12.2
|
typing-extensions==4.13.2
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# alembic
|
# alembic
|
||||||
# apache-superset
|
# apache-superset
|
||||||
# cattrs
|
# cattrs
|
||||||
|
# exceptiongroup
|
||||||
# limits
|
# limits
|
||||||
# pyopenssl
|
# pyopenssl
|
||||||
# referencing
|
# referencing
|
||||||
@@ -857,7 +858,7 @@ tzdata==2025.2
|
|||||||
# pandas
|
# pandas
|
||||||
tzlocal==5.2
|
tzlocal==5.2
|
||||||
# via trino
|
# via trino
|
||||||
url-normalize==1.4.3
|
url-normalize==2.2.1
|
||||||
# via
|
# via
|
||||||
# -c requirements/base.txt
|
# -c requirements/base.txt
|
||||||
# requests-cache
|
# requests-cache
|
||||||
|
|||||||
@@ -252,4 +252,215 @@ 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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
850
superset-frontend/package-lock.json
generated
850
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -122,7 +122,7 @@
|
|||||||
"@visx/tooltip": "^3.0.0",
|
"@visx/tooltip": "^3.0.0",
|
||||||
"@visx/xychart": "^3.5.1",
|
"@visx/xychart": "^3.5.1",
|
||||||
"abortcontroller-polyfill": "^1.7.8",
|
"abortcontroller-polyfill": "^1.7.8",
|
||||||
"ace-builds": "^1.36.3",
|
"ace-builds": "^1.41.0",
|
||||||
"ag-grid-community": "33.1.1",
|
"ag-grid-community": "33.1.1",
|
||||||
"ag-grid-react": "33.1.1",
|
"ag-grid-react": "33.1.1",
|
||||||
"antd": "4.10.3",
|
"antd": "4.10.3",
|
||||||
@@ -137,6 +137,7 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dom-to-image-more": "^3.2.0",
|
"dom-to-image-more": "^3.2.0",
|
||||||
"dom-to-pdf": "^0.3.2",
|
"dom-to-pdf": "^0.3.2",
|
||||||
|
"dompurify": "^3.2.4",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"emotion-rgba": "0.0.12",
|
"emotion-rgba": "0.0.12",
|
||||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||||
@@ -230,7 +231,7 @@
|
|||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
||||||
"@babel/plugin-transform-runtime": "^7.27.1",
|
"@babel/plugin-transform-runtime": "^7.27.1",
|
||||||
"@babel/preset-env": "^7.26.7",
|
"@babel/preset-env": "^7.27.2",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@babel/preset-typescript": "^7.26.0",
|
"@babel/preset-typescript": "^7.26.0",
|
||||||
"@babel/register": "^7.23.7",
|
"@babel/register": "^7.23.7",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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 = {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export interface ChartMetadataConfig {
|
|||||||
label?: ChartLabel | null;
|
label?: ChartLabel | null;
|
||||||
labelExplanation?: string | null;
|
labelExplanation?: string | null;
|
||||||
queryObjectCount?: number;
|
queryObjectCount?: number;
|
||||||
|
dynamicQueryObjectCount?: boolean;
|
||||||
parseMethod?: ParseMethod;
|
parseMethod?: ParseMethod;
|
||||||
// suppressContextMenu: true hides the default context menu for the chart.
|
// suppressContextMenu: true hides the default context menu for the chart.
|
||||||
// This is useful for viz plugins that define their own context menu.
|
// This is useful for viz plugins that define their own context menu.
|
||||||
@@ -92,6 +93,8 @@ export default class ChartMetadata {
|
|||||||
|
|
||||||
queryObjectCount: number;
|
queryObjectCount: number;
|
||||||
|
|
||||||
|
dynamicQueryObjectCount: boolean;
|
||||||
|
|
||||||
parseMethod: ParseMethod;
|
parseMethod: ParseMethod;
|
||||||
|
|
||||||
suppressContextMenu?: boolean;
|
suppressContextMenu?: boolean;
|
||||||
@@ -115,6 +118,7 @@ export default class ChartMetadata {
|
|||||||
label = null,
|
label = null,
|
||||||
labelExplanation = null,
|
labelExplanation = null,
|
||||||
queryObjectCount = 1,
|
queryObjectCount = 1,
|
||||||
|
dynamicQueryObjectCount = false,
|
||||||
parseMethod = 'json-bigint',
|
parseMethod = 'json-bigint',
|
||||||
suppressContextMenu = false,
|
suppressContextMenu = false,
|
||||||
} = config;
|
} = config;
|
||||||
@@ -145,6 +149,7 @@ export default class ChartMetadata {
|
|||||||
this.label = label;
|
this.label = label;
|
||||||
this.labelExplanation = labelExplanation;
|
this.labelExplanation = labelExplanation;
|
||||||
this.queryObjectCount = queryObjectCount;
|
this.queryObjectCount = queryObjectCount;
|
||||||
|
this.dynamicQueryObjectCount = dynamicQueryObjectCount;
|
||||||
this.parseMethod = parseMethod;
|
this.parseMethod = parseMethod;
|
||||||
this.suppressContextMenu = suppressContextMenu;
|
this.suppressContextMenu = suppressContextMenu;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,17 +58,18 @@ export default async function parseResponse<T extends ParseMethod = 'json'>(
|
|||||||
const result: JsonResponse = {
|
const result: JsonResponse = {
|
||||||
response,
|
response,
|
||||||
json: cloneDeepWith(json, (value: any) => {
|
json: cloneDeepWith(json, (value: any) => {
|
||||||
// `json-bigint` could not handle floats well, see sidorares/json-bigint#62
|
|
||||||
// TODO: clean up after json-bigint>1.0.1 is released
|
|
||||||
if (value?.isInteger?.() === false) {
|
|
||||||
return Number(value);
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
|
value?.isInteger?.() === true &&
|
||||||
value?.isLessThan?.(Number.MIN_SAFE_INTEGER)
|
(value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
|
||||||
|
value?.isLessThan?.(Number.MIN_SAFE_INTEGER))
|
||||||
) {
|
) {
|
||||||
return BigInt(value);
|
return BigInt(value);
|
||||||
}
|
}
|
||||||
|
// // `json-bigint` could not handle floats well, see sidorares/json-bigint#62
|
||||||
|
// // TODO: clean up after json-bigint>1.0.1 is released
|
||||||
|
if (value?.isNaN?.() === false) {
|
||||||
|
return value?.toNumber?.();
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ 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';
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
maxValueWithoutServerPagination: number,
|
||||||
|
maxServer: number,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
Number(v) > +maxValueWithoutServerPagination &&
|
||||||
|
Number(v) <= maxServer &&
|
||||||
|
!serverPagination
|
||||||
|
) {
|
||||||
|
return t(
|
||||||
|
'Server pagination needs to be enabled for values over %s',
|
||||||
|
maxValueWithoutServerPagination,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -143,7 +143,7 @@ describe('parseResponse()', () => {
|
|||||||
const mockBigIntUrl = '/mock/get/bigInt';
|
const mockBigIntUrl = '/mock/get/bigInt';
|
||||||
const mockGetBigIntPayload = `{
|
const mockGetBigIntPayload = `{
|
||||||
"value": 9223372036854775807, "minus": { "value": -483729382918228373892, "str": "something" },
|
"value": 9223372036854775807, "minus": { "value": -483729382918228373892, "str": "something" },
|
||||||
"number": 1234, "floatValue": { "plus": 0.3452211361231223, "minus": -0.3452211361231223 },
|
"number": 1234, "floatValue": { "plus": 0.3452211361231223, "minus": -0.3452211361231223, "even": 1234567890123456.0000000 },
|
||||||
"string.constructor": "data.constructor",
|
"string.constructor": "data.constructor",
|
||||||
"constructor": "constructor"
|
"constructor": "constructor"
|
||||||
}`;
|
}`;
|
||||||
@@ -161,6 +161,7 @@ describe('parseResponse()', () => {
|
|||||||
expect(responseBigNumber.json.floatValue.minus).toEqual(
|
expect(responseBigNumber.json.floatValue.minus).toEqual(
|
||||||
-0.3452211361231223,
|
-0.3452211361231223,
|
||||||
);
|
);
|
||||||
|
expect(responseBigNumber.json.floatValue.even).toEqual(1234567890123456);
|
||||||
expect(
|
expect(
|
||||||
responseBigNumber.json.floatValue.plus +
|
responseBigNumber.json.floatValue.plus +
|
||||||
responseBigNumber.json.floatValue.minus,
|
responseBigNumber.json.floatValue.minus,
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
const DEFAULT_MAX_ROW = 100000;
|
||||||
|
const DEFAULT_MAX_ROW_TABLE_SERVER = 500000;
|
||||||
|
|
||||||
|
test('validateServerPagination returns warning message only when value is between max thresholds and server pagination is disabled', () => {
|
||||||
|
// Should show warning - value between thresholds and server pagination disabled
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
200000,
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
300000,
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
// Should not show warning - value above max server threshold
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
600000,
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
|
||||||
|
// Should not show warning - value below max without server threshold
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
50000,
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validateServerPagination returns false when server pagination is enabled regardless of value', () => {
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
200000,
|
||||||
|
true,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
300000,
|
||||||
|
true,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
600000,
|
||||||
|
true,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validateServerPagination handles string inputs correctly', () => {
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
'200000',
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
'600000',
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
'50000',
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validateServerPagination handles edge cases', () => {
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
NaN,
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
validateServerPagination(
|
||||||
|
'invalid',
|
||||||
|
false,
|
||||||
|
DEFAULT_MAX_ROW,
|
||||||
|
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
"@babel/preset-env": "^7.26.7",
|
"@babel/preset-env": "^7.27.2",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@babel/preset-typescript": "^7.23.3",
|
"@babel/preset-typescript": "^7.23.3",
|
||||||
"@storybook/react-webpack5": "8.2.9",
|
"@storybook/react-webpack5": "8.2.9",
|
||||||
|
|||||||
@@ -38,9 +38,20 @@ import {
|
|||||||
} from '../DeckGLContainer';
|
} from '../DeckGLContainer';
|
||||||
import { getExploreLongUrl } from '../utils/explore';
|
import { getExploreLongUrl } from '../utils/explore';
|
||||||
import layerGenerators from '../layers';
|
import layerGenerators from '../layers';
|
||||||
import { Viewport } from '../utils/fitViewport';
|
import fitViewport, { Viewport } from '../utils/fitViewport';
|
||||||
import { TooltipProps } from '../components/Tooltip';
|
import { TooltipProps } from '../components/Tooltip';
|
||||||
|
|
||||||
|
import { getPoints as getPointsArc } from '../layers/Arc/Arc';
|
||||||
|
import { getPoints as getPointsPath } from '../layers/Path/Path';
|
||||||
|
import { getPoints as getPointsPolygon } from '../layers/Polygon/Polygon';
|
||||||
|
import { getPoints as getPointsGrid } from '../layers/Grid/Grid';
|
||||||
|
import { getPoints as getPointsScatter } from '../layers/Scatter/Scatter';
|
||||||
|
import { getPoints as getPointsContour } from '../layers/Contour/Contour';
|
||||||
|
import { getPoints as getPointsHeatmap } from '../layers/Heatmap/Heatmap';
|
||||||
|
import { getPoints as getPointsHex } from '../layers/Hex/Hex';
|
||||||
|
import { getPoints as getPointsGeojson } from '../layers/Geojson/Geojson';
|
||||||
|
import { getPoints as getPointsScreengrid } from '../layers/Screengrid/Screengrid';
|
||||||
|
|
||||||
export type DeckMultiProps = {
|
export type DeckMultiProps = {
|
||||||
formData: QueryFormData;
|
formData: QueryFormData;
|
||||||
payload: JsonObject;
|
payload: JsonObject;
|
||||||
@@ -56,7 +67,35 @@ export type DeckMultiProps = {
|
|||||||
const DeckMulti = (props: DeckMultiProps) => {
|
const DeckMulti = (props: DeckMultiProps) => {
|
||||||
const containerRef = useRef<DeckGLContainerHandle>();
|
const containerRef = useRef<DeckGLContainerHandle>();
|
||||||
|
|
||||||
const [viewport, setViewport] = useState<Viewport>();
|
const getAdjustedViewport = useCallback(() => {
|
||||||
|
let viewport = { ...props.viewport };
|
||||||
|
const points = [
|
||||||
|
...getPointsPolygon(props.payload.data.features.deck_polygon || []),
|
||||||
|
...getPointsPath(props.payload.data.features.deck_path || []),
|
||||||
|
...getPointsGrid(props.payload.data.features.deck_grid || []),
|
||||||
|
...getPointsScatter(props.payload.data.features.deck_scatter || []),
|
||||||
|
...getPointsContour(props.payload.data.features.deck_contour || []),
|
||||||
|
...getPointsHeatmap(props.payload.data.features.deck_heatmap || []),
|
||||||
|
...getPointsHex(props.payload.data.features.deck_hex || []),
|
||||||
|
...getPointsArc(props.payload.data.features.deck_arc || []),
|
||||||
|
...getPointsGeojson(props.payload.data.features.deck_geojson || []),
|
||||||
|
...getPointsScreengrid(props.payload.data.features.deck_screengrid || []),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (props.formData) {
|
||||||
|
viewport = fitViewport(viewport, {
|
||||||
|
width: props.width,
|
||||||
|
height: props.height,
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (viewport.zoom < 0) {
|
||||||
|
viewport.zoom = 0;
|
||||||
|
}
|
||||||
|
return viewport;
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
|
const [viewport, setViewport] = useState<Viewport>(getAdjustedViewport());
|
||||||
const [subSlicesLayers, setSubSlicesLayers] = useState<Record<number, Layer>>(
|
const [subSlicesLayers, setSubSlicesLayers] = useState<Record<number, Layer>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@@ -70,23 +109,31 @@ const DeckMulti = (props: DeckMultiProps) => {
|
|||||||
|
|
||||||
const loadLayers = useCallback(
|
const loadLayers = useCallback(
|
||||||
(formData: QueryFormData, payload: JsonObject, viewport?: Viewport) => {
|
(formData: QueryFormData, payload: JsonObject, viewport?: Viewport) => {
|
||||||
setViewport(viewport);
|
setViewport(getAdjustedViewport());
|
||||||
setSubSlicesLayers({});
|
setSubSlicesLayers({});
|
||||||
payload.data.slices.forEach(
|
payload.data.slices.forEach(
|
||||||
(subslice: { slice_id: number } & JsonObject) => {
|
(subslice: { slice_id: number } & JsonObject) => {
|
||||||
// Filters applied to multi_deck are passed down to underlying charts
|
// Filters applied to multi_deck are passed down to underlying charts
|
||||||
// note that dashboard contextual information (filter_immune_slices and such) aren't
|
// note that dashboard contextual information (filter_immune_slices and such) aren't
|
||||||
// taken into consideration here
|
// taken into consideration here
|
||||||
const filters = [
|
const extra_filters = [
|
||||||
...(subslice.form_data.filters || []),
|
...(subslice.form_data.extra_filters || []),
|
||||||
...(formData.filters || []),
|
|
||||||
...(formData.extra_filters || []),
|
...(formData.extra_filters || []),
|
||||||
|
...(formData.extra_form_data?.filters || []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const adhoc_filters = [
|
||||||
|
...(formData.adhoc_filters || []),
|
||||||
|
...(subslice.formData?.adhoc_filters || []),
|
||||||
|
...(formData.extra_form_data?.adhoc_filters || []),
|
||||||
|
];
|
||||||
|
|
||||||
const subsliceCopy = {
|
const subsliceCopy = {
|
||||||
...subslice,
|
...subslice,
|
||||||
form_data: {
|
form_data: {
|
||||||
...subslice.form_data,
|
...subslice.form_data,
|
||||||
filters,
|
extra_filters,
|
||||||
|
adhoc_filters,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,7 +164,13 @@ const DeckMulti = (props: DeckMultiProps) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[props.datasource, props.onAddFilter, props.onSelect, setTooltip],
|
[
|
||||||
|
props.datasource,
|
||||||
|
props.onAddFilter,
|
||||||
|
props.onSelect,
|
||||||
|
setTooltip,
|
||||||
|
getAdjustedViewport,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const prevDeckSlices = usePrevious(props.formData.deck_slices);
|
const prevDeckSlices = usePrevious(props.formData.deck_slices);
|
||||||
@@ -136,7 +189,7 @@ const DeckMulti = (props: DeckMultiProps) => {
|
|||||||
<DeckGLContainerStyledWrapper
|
<DeckGLContainerStyledWrapper
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||||
viewport={viewport || props.viewport}
|
viewport={viewport}
|
||||||
layers={layers}
|
layers={layers}
|
||||||
mapStyle={formData.mapbox_style}
|
mapStyle={formData.mapbox_style}
|
||||||
setControlValue={setControlValue}
|
setControlValue={setControlValue}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import TooltipRow from '../../TooltipRow';
|
|||||||
import { TooltipProps } from '../../components/Tooltip';
|
import { TooltipProps } from '../../components/Tooltip';
|
||||||
import { Point } from '../../types';
|
import { Point } from '../../types';
|
||||||
|
|
||||||
function getPoints(data: JsonObject[]) {
|
export function getPoints(data: JsonObject[]) {
|
||||||
const points: Point[] = [];
|
const points: Point[] = [];
|
||||||
data.forEach(d => {
|
data.forEach(d => {
|
||||||
points.push(d.sourcePosition);
|
points.push(d.sourcePosition);
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const getLayer: getLayerType<unknown> = function (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPoints(data: any[]) {
|
export function getPoints(data: any[]) {
|
||||||
return data.map(d => d.position);
|
return data.map(d => d.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { commonLayerProps } from '../common';
|
|||||||
import TooltipRow from '../../TooltipRow';
|
import TooltipRow from '../../TooltipRow';
|
||||||
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||||
import { TooltipProps } from '../../components/Tooltip';
|
import { TooltipProps } from '../../components/Tooltip';
|
||||||
|
import { Point } from '../../types';
|
||||||
|
|
||||||
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
|
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
|
||||||
properties: JsonObject;
|
properties: JsonObject;
|
||||||
@@ -172,6 +173,17 @@ export type DeckGLGeoJsonProps = {
|
|||||||
width: number;
|
width: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getPoints(data: Point[]) {
|
||||||
|
return data.reduce((acc: Array<any>, feature: any) => {
|
||||||
|
const bounds = geojsonExtent(feature);
|
||||||
|
if (bounds) {
|
||||||
|
return [...acc, [bounds[0], bounds[1]], [bounds[2], bounds[3]]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||||
const containerRef = useRef<DeckGLContainerHandle>();
|
const containerRef = useRef<DeckGLContainerHandle>();
|
||||||
const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
|
const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
|
||||||
@@ -186,24 +198,13 @@ const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
|||||||
|
|
||||||
const viewport: Viewport = useMemo(() => {
|
const viewport: Viewport = useMemo(() => {
|
||||||
if (formData.autozoom) {
|
if (formData.autozoom) {
|
||||||
const points =
|
const points = getPoints(payload.data.features) || [];
|
||||||
payload?.data?.features?.reduce?.(
|
|
||||||
(acc: [number, number, number, number][], feature: any) => {
|
|
||||||
const bounds = geojsonExtent(feature);
|
|
||||||
if (bounds) {
|
|
||||||
return [...acc, [bounds[0], bounds[1]], [bounds[2], bounds[3]]];
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
) || [];
|
|
||||||
|
|
||||||
if (points.length) {
|
if (points.length) {
|
||||||
return fitViewport(props.viewport, {
|
return fitViewport(props.viewport, {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
points,
|
points: getPoints(payload.data.features) || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function getLayer(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPoints(data: JsonObject[]) {
|
export function getPoints(data: JsonObject[]) {
|
||||||
return data.map(d => d.position);
|
return data.map(d => d.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const getLayer: getLayerType<unknown> = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPoints(data: any[]) {
|
export function getPoints(data: any[]) {
|
||||||
return data.map(d => d.position);
|
return data.map(d => d.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function getLayer(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPoints(data: JsonObject[]) {
|
export function getPoints(data: JsonObject[]) {
|
||||||
return data.map(d => d.position);
|
return data.map(d => d.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function getLayer(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPoints(data: JsonObject[]) {
|
export function getPoints(data: JsonObject[]) {
|
||||||
let points: Point[] = [];
|
let points: Point[] = [];
|
||||||
data.forEach(d => {
|
data.forEach(d => {
|
||||||
points = points.concat(d.path);
|
points = points.concat(d.path);
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ export type DeckGLPolygonProps = {
|
|||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getPoints(data: JsonObject[]) {
|
||||||
|
return data.flatMap(getPointsFromPolygon);
|
||||||
|
}
|
||||||
|
|
||||||
const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||||
const containerRef = useRef<DeckGLContainerHandle>();
|
const containerRef = useRef<DeckGLContainerHandle>();
|
||||||
|
|
||||||
@@ -183,7 +187,7 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
|||||||
viewport = fitViewport(viewport, {
|
viewport = fitViewport(viewport, {
|
||||||
width: props.width,
|
width: props.width,
|
||||||
height: props.height,
|
height: props.height,
|
||||||
points: features.flatMap(getPointsFromPolygon),
|
points: getPoints(features),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (viewport.zoom < 0) {
|
if (viewport.zoom < 0) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import TooltipRow from '../../TooltipRow';
|
|||||||
import { unitToRadius } from '../../utils/geo';
|
import { unitToRadius } from '../../utils/geo';
|
||||||
import { TooltipProps } from '../../components/Tooltip';
|
import { TooltipProps } from '../../components/Tooltip';
|
||||||
|
|
||||||
function getPoints(data: JsonObject[]) {
|
export function getPoints(data: JsonObject[]) {
|
||||||
return data.map(d => d.position);
|
return data.map(d => d.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
} from '../../DeckGLContainer';
|
} from '../../DeckGLContainer';
|
||||||
import { TooltipProps } from '../../components/Tooltip';
|
import { TooltipProps } from '../../components/Tooltip';
|
||||||
|
|
||||||
function getPoints(data: JsonObject[]) {
|
export function getPoints(data: JsonObject[]) {
|
||||||
return data.map(d => d.position);
|
return data.map(d => d.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3": "^3.5.17",
|
"d3": "^3.5.17",
|
||||||
"d3-tip": "^0.9.1",
|
"d3-tip": "^0.9.1",
|
||||||
"dompurify": "^3.2.4",
|
|
||||||
"fast-safe-stringify": "^2.1.1",
|
"fast-safe-stringify": "^2.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"nvd3-fork": "^2.0.5",
|
"nvd3-fork": "^2.0.5",
|
||||||
|
"dompurify": "^3.2.4",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"urijs": "^1.19.11"
|
"urijs": "^1.19.11"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,13 +36,25 @@ 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>`
|
||||||
@@ -73,6 +85,8 @@ export default function PopKPI(props: PopKPIProps) {
|
|||||||
prevNumber,
|
prevNumber,
|
||||||
valueDifference,
|
valueDifference,
|
||||||
percentDifferenceFormattedString,
|
percentDifferenceFormattedString,
|
||||||
|
metricName,
|
||||||
|
metricNameFontSize,
|
||||||
headerFontSize,
|
headerFontSize,
|
||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
comparisonColorEnabled,
|
comparisonColorEnabled,
|
||||||
@@ -84,8 +98,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(() => {
|
||||||
@@ -260,9 +274,16 @@ 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,6 +28,8 @@ import {
|
|||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
subtitleControl,
|
subtitleControl,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
|
showMetricNameControl,
|
||||||
|
metricNameFontSizeWithVisibility,
|
||||||
} from '../sharedControls';
|
} from '../sharedControls';
|
||||||
import { ColorSchemeEnum } from './types';
|
import { ColorSchemeEnum } from './types';
|
||||||
|
|
||||||
@@ -70,6 +72,8 @@ const config: ControlPanelConfig = {
|
|||||||
],
|
],
|
||||||
[subtitleControl],
|
[subtitleControl],
|
||||||
[subtitleFontSize],
|
[subtitleFontSize],
|
||||||
|
[showMetricNameControl],
|
||||||
|
[metricNameFontSizeWithVisibility],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
...subheaderFontSize,
|
...subheaderFontSize,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ 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,7 +26,13 @@ import {
|
|||||||
SimpleAdhocFilter,
|
SimpleAdhocFilter,
|
||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { getComparisonFontSize, getHeaderFontSize } from './utils';
|
import {
|
||||||
|
getComparisonFontSize,
|
||||||
|
getHeaderFontSize,
|
||||||
|
getMetricNameFontSize,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
import { getOriginalLabel } from '../utils';
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@@ -83,6 +89,7 @@ export default function transformProps(chartProps: ChartProps) {
|
|||||||
headerFontSize,
|
headerFontSize,
|
||||||
headerText,
|
headerText,
|
||||||
metric,
|
metric,
|
||||||
|
metricNameFontSize,
|
||||||
yAxisFormat,
|
yAxisFormat,
|
||||||
currencyFormat,
|
currencyFormat,
|
||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
@@ -91,11 +98,14 @@ 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(
|
||||||
@@ -179,7 +189,7 @@ export default function transformProps(chartProps: ChartProps) {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
data,
|
data,
|
||||||
metricName,
|
metricName: originalLabel,
|
||||||
bigNumber,
|
bigNumber,
|
||||||
prevNumber,
|
prevNumber,
|
||||||
valueDifference,
|
valueDifference,
|
||||||
@@ -187,6 +197,8 @@ 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,6 +61,8 @@ 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,10 +16,19 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { headerFontSize, subheaderFontSize } from '../sharedControls';
|
import {
|
||||||
|
headerFontSize,
|
||||||
|
subheaderFontSize,
|
||||||
|
metricNameFontSize,
|
||||||
|
} from '../sharedControls';
|
||||||
|
|
||||||
const headerFontSizes = [16, 20, 30, 48, 60];
|
const headerFontSizes = [16, 20, 30, 48, 60];
|
||||||
const comparisonFontSizes = [16, 20, 26, 32, 40];
|
const sharedFontSizes = [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(
|
||||||
@@ -40,6 +49,10 @@ const getFontSizeMapping = (
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const metricNameFontSizesMapping = getFontSizeMapping(
|
||||||
|
metricNameProportionValues,
|
||||||
|
sharedFontSizes,
|
||||||
|
);
|
||||||
const headerFontSizesMapping = getFontSizeMapping(
|
const headerFontSizesMapping = getFontSizeMapping(
|
||||||
headerProportionValues,
|
headerProportionValues,
|
||||||
headerFontSizes,
|
headerFontSizes,
|
||||||
@@ -47,13 +60,17 @@ const headerFontSizesMapping = getFontSizeMapping(
|
|||||||
|
|
||||||
const comparisonFontSizesMapping = getFontSizeMapping(
|
const comparisonFontSizesMapping = getFontSizeMapping(
|
||||||
subheaderProportionValues,
|
subheaderProportionValues,
|
||||||
comparisonFontSizes,
|
sharedFontSizes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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] ??
|
||||||
comparisonFontSizes[comparisonFontSizes.length - 1];
|
sharedFontSizes[sharedFontSizes.length - 1];
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import {
|
|||||||
headerFontSize,
|
headerFontSize,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
subtitleControl,
|
subtitleControl,
|
||||||
|
showMetricNameControl,
|
||||||
|
metricNameFontSizeWithVisibility,
|
||||||
} from '../sharedControls';
|
} from '../sharedControls';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -44,6 +46,8 @@ export default {
|
|||||||
[headerFontSize],
|
[headerFontSize],
|
||||||
[subtitleControl],
|
[subtitleControl],
|
||||||
[subtitleFontSize],
|
[subtitleFontSize],
|
||||||
|
[showMetricNameControl],
|
||||||
|
[metricNameFontSizeWithVisibility],
|
||||||
['y_axis_format'],
|
['y_axis_format'],
|
||||||
['currency_format'],
|
['currency_format'],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ 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,6 +36,7 @@ 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, parseMetricValue } from '../utils';
|
import { getDateFormatter, getOriginalLabel, parseMetricValue } from '../utils';
|
||||||
import { Refs } from '../../types';
|
import { Refs } from '../../types';
|
||||||
|
|
||||||
export default function transformProps(
|
export default function transformProps(
|
||||||
@@ -45,6 +45,7 @@ export default function transformProps(
|
|||||||
datasource: { currencyFormats = {}, columnFormats = {} },
|
datasource: { currencyFormats = {}, columnFormats = {} },
|
||||||
} = chartProps;
|
} = chartProps;
|
||||||
const {
|
const {
|
||||||
|
metricNameFontSize,
|
||||||
headerFontSize,
|
headerFontSize,
|
||||||
metric = 'value',
|
metric = 'value',
|
||||||
subtitle,
|
subtitle,
|
||||||
@@ -58,9 +59,12 @@ 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)
|
||||||
@@ -103,7 +107,6 @@ export default function transformProps(
|
|||||||
const colorThresholdFormatters =
|
const colorThresholdFormatters =
|
||||||
getColorFormatters(conditionalFormatting, data, false) ??
|
getColorFormatters(conditionalFormatting, data, false) ??
|
||||||
defaultColorFormatters;
|
defaultColorFormatters;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -116,5 +119,8 @@ 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 } from 'react';
|
import { PureComponent, MouseEvent, createRef } from 'react';
|
||||||
import {
|
import {
|
||||||
t,
|
t,
|
||||||
getNumberFormatter,
|
getNumberFormatter,
|
||||||
@@ -35,6 +35,7 @@ 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,
|
||||||
@@ -42,13 +43,20 @@ const PROPORTION = {
|
|||||||
TRENDLINE: 0.3,
|
TRENDLINE: 0.3,
|
||||||
};
|
};
|
||||||
|
|
||||||
class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
type BigNumberVisState = {
|
||||||
|
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,
|
||||||
@@ -58,6 +66,40 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
|||||||
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} ${
|
||||||
@@ -92,6 +134,37 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
@@ -118,6 +191,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={this.kickerRef}
|
||||||
className="kicker"
|
className="kicker"
|
||||||
style={{
|
style={{
|
||||||
fontSize,
|
fontSize,
|
||||||
@@ -173,6 +247,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={this.headerRef}
|
||||||
className="header-line"
|
className="header-line"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -211,6 +286,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={this.subheaderRef}
|
||||||
className="subheader-line"
|
className="subheader-line"
|
||||||
style={{
|
style={{
|
||||||
fontSize,
|
fontSize,
|
||||||
@@ -256,6 +332,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
|||||||
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`,
|
||||||
@@ -316,6 +393,35 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
@@ -323,6 +429,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
|||||||
kickerFontSize,
|
kickerFontSize,
|
||||||
headerFontSize,
|
headerFontSize,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
|
metricNameFontSize,
|
||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const className = this.getClassName();
|
const className = this.getClassName();
|
||||||
@@ -330,11 +437,31 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
|||||||
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 className="text-container" style={{ height: allTextHeight }}>
|
<div
|
||||||
|
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,
|
||||||
@@ -356,16 +483,33 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const shouldApplyOverflow = this.shouldApplyOverflow(height);
|
||||||
return (
|
return (
|
||||||
<div className={className} style={{ height }}>
|
<div
|
||||||
{this.renderFallbackWarning()}
|
className={className}
|
||||||
{this.renderKicker((kickerFontSize || 0) * height)}
|
style={{
|
||||||
{this.renderHeader(Math.ceil(headerFontSize * height))}
|
height,
|
||||||
{this.rendermetricComparisonSummary(
|
...(shouldApplyOverflow
|
||||||
Math.ceil(subheaderFontSize * height),
|
? {
|
||||||
)}
|
display: 'block',
|
||||||
{this.renderSubtitle(Math.ceil(subtitleFontSize * height))}
|
boxSizing: 'border-box',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
overflowY: 'auto',
|
||||||
|
width: '100%',
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-container">
|
||||||
|
{this.renderFallbackWarning()}
|
||||||
|
{this.renderMetricName((metricNameFontSize || 0) * height)}
|
||||||
|
{this.renderKicker((kickerFontSize || 0) * height)}
|
||||||
|
{this.renderHeader(Math.ceil(headerFontSize * height))}
|
||||||
|
{this.rendermetricComparisonSummary(
|
||||||
|
Math.ceil(subheaderFontSize * height),
|
||||||
|
)}
|
||||||
|
{this.renderSubtitle(Math.ceil(subtitleFontSize * height))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -400,7 +544,12 @@ export default styled(BigNumberVis)`
|
|||||||
|
|
||||||
.kicker {
|
.kicker {
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
padding-bottom: 2em;
|
margin-bottom: ${theme.gridUnit * 2}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-name {
|
||||||
|
line-height: 1em;
|
||||||
|
margin-bottom: ${theme.gridUnit * 2}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-line {
|
.header-line {
|
||||||
@@ -416,12 +565,12 @@ export default styled(BigNumberVis)`
|
|||||||
|
|
||||||
.subheader-line {
|
.subheader-line {
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
padding-bottom: 0;
|
margin-bottom: ${theme.gridUnit * 2}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle-line {
|
.subtitle-line {
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
padding-bottom: 0;
|
margin-bottom: ${theme.gridUnit * 2}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-fallback-value {
|
&.is-fallback-value {
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import {
|
|||||||
subheaderFontSize,
|
subheaderFontSize,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
subtitleControl,
|
subtitleControl,
|
||||||
|
showMetricNameControl,
|
||||||
|
metricNameFontSizeWithVisibility,
|
||||||
} from '../sharedControls';
|
} from '../sharedControls';
|
||||||
|
|
||||||
const config: ControlPanelConfig = {
|
const config: ControlPanelConfig = {
|
||||||
@@ -141,6 +143,8 @@ const config: ControlPanelConfig = {
|
|||||||
[subheaderFontSize],
|
[subheaderFontSize],
|
||||||
[subtitleControl],
|
[subtitleControl],
|
||||||
[subtitleFontSize],
|
[subtitleFontSize],
|
||||||
|
[showMetricNameControl],
|
||||||
|
[metricNameFontSizeWithVisibility],
|
||||||
['y_axis_format'],
|
['y_axis_format'],
|
||||||
['currency_format'],
|
['currency_format'],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ 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,6 +39,7 @@ 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 } from '../utils';
|
import { getDateFormatter, parseMetricValue, getOriginalLabel } from '../utils';
|
||||||
import { getDefaultTooltip } from '../../utils/tooltip';
|
import { getDefaultTooltip } from '../../utils/tooltip';
|
||||||
import { Refs } from '../../types';
|
import { Refs } from '../../types';
|
||||||
|
|
||||||
@@ -62,6 +62,7 @@ export default function transformProps(
|
|||||||
compareLag: compareLag_,
|
compareLag: compareLag_,
|
||||||
compareSuffix = '',
|
compareSuffix = '',
|
||||||
timeFormat,
|
timeFormat,
|
||||||
|
metricNameFontSize,
|
||||||
headerFontSize,
|
headerFontSize,
|
||||||
metric = 'value',
|
metric = 'value',
|
||||||
showTimestamp,
|
showTimestamp,
|
||||||
@@ -96,6 +97,9 @@ 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;
|
||||||
|
|
||||||
@@ -303,6 +307,9 @@ export default function transformProps(
|
|||||||
headerFormatter,
|
headerFormatter,
|
||||||
formatTime,
|
formatTime,
|
||||||
formData,
|
formData,
|
||||||
|
metricName: originalLabel,
|
||||||
|
showMetricName,
|
||||||
|
metricNameFontSize,
|
||||||
headerFontSize,
|
headerFontSize,
|
||||||
subtitleFontSize,
|
subtitleFontSize,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
|||||||
@@ -21,106 +21,68 @@
|
|||||||
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';
|
||||||
|
|
||||||
export const headerFontSize: CustomControlItem = {
|
const FONT_SIZE_OPTIONS_SMALL = [
|
||||||
name: 'header_font_size',
|
{ label: t('Tiny'), value: 0.125 },
|
||||||
config: {
|
{ label: t('Small'), value: 0.15 },
|
||||||
type: 'SelectControl',
|
{ label: t('Normal'), value: 0.2 },
|
||||||
label: t('Big Number Font Size'),
|
{ label: t('Large'), value: 0.3 },
|
||||||
renderTrigger: true,
|
{ label: t('Huge'), value: 0.4 },
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const subtitleFontSize: CustomControlItem = {
|
const FONT_SIZE_OPTIONS_LARGE = [
|
||||||
name: 'subtitle_font_size',
|
{ label: t('Tiny'), value: 0.2 },
|
||||||
config: {
|
{ label: t('Small'), value: 0.3 },
|
||||||
type: 'SelectControl',
|
{ label: t('Normal'), value: 0.4 },
|
||||||
label: t('Subtitle Font Size'),
|
{ label: t('Large'), value: 0.5 },
|
||||||
renderTrigger: true,
|
{ label: t('Huge'), value: 0.6 },
|
||||||
clearable: false,
|
];
|
||||||
default: 0.15,
|
|
||||||
// Values represent the percentage of space a subtitle should take
|
function makeFontSizeControl(
|
||||||
options: [
|
name: string,
|
||||||
{
|
label: string,
|
||||||
label: t('Tiny'),
|
defaultValue: number,
|
||||||
value: 0.125,
|
options: { label: string; value: number }[],
|
||||||
},
|
): CustomControlItem {
|
||||||
{
|
return {
|
||||||
label: t('Small'),
|
name,
|
||||||
value: 0.15,
|
config: {
|
||||||
},
|
type: 'SelectControl',
|
||||||
{
|
label: t(label),
|
||||||
label: t('Normal'),
|
renderTrigger: true,
|
||||||
value: 0.2,
|
clearable: false,
|
||||||
},
|
default: defaultValue,
|
||||||
{
|
options,
|
||||||
label: t('Large'),
|
},
|
||||||
value: 0.3,
|
};
|
||||||
},
|
}
|
||||||
{
|
|
||||||
label: t('Huge'),
|
export const headerFontSize = makeFontSizeControl(
|
||||||
value: 0.4,
|
'header_font_size',
|
||||||
},
|
'Big Number Font Size',
|
||||||
],
|
0.4,
|
||||||
},
|
FONT_SIZE_OPTIONS_LARGE,
|
||||||
};
|
);
|
||||||
export const subheaderFontSize: CustomControlItem = {
|
|
||||||
name: 'subheader_font_size',
|
export const subtitleFontSize = makeFontSizeControl(
|
||||||
config: {
|
'subtitle_font_size',
|
||||||
type: 'SelectControl',
|
'Subtitle Font Size',
|
||||||
label: t('Subheader Font Size'),
|
0.15,
|
||||||
renderTrigger: true,
|
FONT_SIZE_OPTIONS_SMALL,
|
||||||
clearable: false,
|
);
|
||||||
default: 0.15,
|
|
||||||
// Values represent the percentage of space a subheader should take
|
export const subheaderFontSize = makeFontSizeControl(
|
||||||
options: [
|
'subheader_font_size',
|
||||||
{
|
'Subheader Font Size',
|
||||||
label: t('Tiny'),
|
0.15,
|
||||||
value: 0.125,
|
FONT_SIZE_OPTIONS_SMALL,
|
||||||
},
|
);
|
||||||
{
|
|
||||||
label: t('Small'),
|
export const metricNameFontSize = makeFontSizeControl(
|
||||||
value: 0.15,
|
'metric_name_font_size',
|
||||||
},
|
'Metric Name Font Size',
|
||||||
{
|
0.15,
|
||||||
label: t('Normal'),
|
FONT_SIZE_OPTIONS_SMALL,
|
||||||
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',
|
||||||
@@ -131,3 +93,23 @@ 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,6 +75,10 @@ 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,6 +22,10 @@ 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';
|
||||||
@@ -47,3 +51,43 @@ 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';
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,14 +16,30 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { buildQueryContext, QueryFormData } from '@superset-ui/core';
|
import {
|
||||||
|
buildQueryContext,
|
||||||
|
getMetricLabel,
|
||||||
|
QueryFormData,
|
||||||
|
} from '@superset-ui/core';
|
||||||
|
import { getContributionLabel } from './utils';
|
||||||
|
|
||||||
export default function buildQuery(formData: QueryFormData) {
|
export default function buildQuery(formData: QueryFormData) {
|
||||||
const { metric, sort_by_metric } = formData;
|
const { metric, sort_by_metric } = formData;
|
||||||
|
const metricLabel = getMetricLabel(metric);
|
||||||
|
|
||||||
return buildQueryContext(formData, baseQueryObject => [
|
return buildQueryContext(formData, baseQueryObject => [
|
||||||
{
|
{
|
||||||
...baseQueryObject,
|
...baseQueryObject,
|
||||||
...(sort_by_metric && { orderby: [[metric, false]] }),
|
...(sort_by_metric && { orderby: [[metric, false]] }),
|
||||||
|
post_processing: [
|
||||||
|
{
|
||||||
|
operation: 'contribution',
|
||||||
|
options: {
|
||||||
|
columns: [metricLabel],
|
||||||
|
rename_columns: [getContributionLabel(metricLabel)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* 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 CONTRIBUTION_SUFFIX = '__contribution' as const;
|
||||||
@@ -84,6 +84,23 @@ const config: ControlPanelConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'threshold_for_other',
|
||||||
|
config: {
|
||||||
|
type: 'NumberControl',
|
||||||
|
label: t('Threshold for Other'),
|
||||||
|
min: 0,
|
||||||
|
step: 0.5,
|
||||||
|
max: 100,
|
||||||
|
default: 0,
|
||||||
|
renderTrigger: true,
|
||||||
|
description: t(
|
||||||
|
'Values less than this percentage will be grouped into the Other category.',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: 'roseType',
|
name: 'roseType',
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
ValueFormatter,
|
ValueFormatter,
|
||||||
getValueFormatter,
|
getValueFormatter,
|
||||||
tooltipHtml,
|
tooltipHtml,
|
||||||
|
DataRecord,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import type { CallbackDataParams } from 'echarts/types/src/util/types';
|
import type { CallbackDataParams } from 'echarts/types/src/util/types';
|
||||||
import type { EChartsCoreOption } from 'echarts/core';
|
import type { EChartsCoreOption } from 'echarts/core';
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
EchartsPieChartProps,
|
EchartsPieChartProps,
|
||||||
EchartsPieFormData,
|
EchartsPieFormData,
|
||||||
EchartsPieLabelType,
|
EchartsPieLabelType,
|
||||||
|
PieChartDataItem,
|
||||||
PieChartTransformedProps,
|
PieChartTransformedProps,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants';
|
import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants';
|
||||||
@@ -50,6 +52,7 @@ import { defaultGrid } from '../defaults';
|
|||||||
import { convertInteger } from '../utils/convertInteger';
|
import { convertInteger } from '../utils/convertInteger';
|
||||||
import { getDefaultTooltip } from '../utils/tooltip';
|
import { getDefaultTooltip } from '../utils/tooltip';
|
||||||
import { Refs } from '../types';
|
import { Refs } from '../types';
|
||||||
|
import { getContributionLabel } from './utils';
|
||||||
|
|
||||||
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
|
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
|
||||||
|
|
||||||
@@ -133,7 +136,7 @@ export default function transformProps(
|
|||||||
datasource,
|
datasource,
|
||||||
} = chartProps;
|
} = chartProps;
|
||||||
const { columnFormats = {}, currencyFormats = {} } = datasource;
|
const { columnFormats = {}, currencyFormats = {} } = datasource;
|
||||||
const { data = [] } = queriesData[0];
|
const { data: rawData = [] } = queriesData[0];
|
||||||
const coltypeMapping = getColtypesMapping(queriesData[0]);
|
const coltypeMapping = getColtypesMapping(queriesData[0]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -159,6 +162,7 @@ export default function transformProps(
|
|||||||
sliceId,
|
sliceId,
|
||||||
showTotal,
|
showTotal,
|
||||||
roseType,
|
roseType,
|
||||||
|
thresholdForOther,
|
||||||
}: EchartsPieFormData = {
|
}: EchartsPieFormData = {
|
||||||
...DEFAULT_LEGEND_FORM_DATA,
|
...DEFAULT_LEGEND_FORM_DATA,
|
||||||
...DEFAULT_PIE_FORM_DATA,
|
...DEFAULT_PIE_FORM_DATA,
|
||||||
@@ -166,17 +170,68 @@ export default function transformProps(
|
|||||||
};
|
};
|
||||||
const refs: Refs = {};
|
const refs: Refs = {};
|
||||||
const metricLabel = getMetricLabel(metric);
|
const metricLabel = getMetricLabel(metric);
|
||||||
|
const contributionLabel = getContributionLabel(metricLabel);
|
||||||
const groupbyLabels = groupby.map(getColumnLabel);
|
const groupbyLabels = groupby.map(getColumnLabel);
|
||||||
const minShowLabelAngle = (showLabelsThreshold || 0) * 3.6;
|
const minShowLabelAngle = (showLabelsThreshold || 0) * 3.6;
|
||||||
|
|
||||||
const keys = data.map(datum =>
|
const numberFormatter = getValueFormatter(
|
||||||
extractGroupbyLabel({
|
metric,
|
||||||
datum,
|
currencyFormats,
|
||||||
groupby: groupbyLabels,
|
columnFormats,
|
||||||
coltypeMapping,
|
numberFormat,
|
||||||
timeFormatter: getTimeFormatter(dateFormat),
|
currencyFormat,
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let data = rawData;
|
||||||
|
const otherRows: DataRecord[] = [];
|
||||||
|
const otherTooltipData: string[][] = [];
|
||||||
|
let otherDatum: PieChartDataItem | null = null;
|
||||||
|
let otherSum = 0;
|
||||||
|
if (thresholdForOther) {
|
||||||
|
let contributionSum = 0;
|
||||||
|
data = data.filter(datum => {
|
||||||
|
const contribution = datum[contributionLabel] as number;
|
||||||
|
if (!contribution || contribution * 100 >= thresholdForOther) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
otherSum += datum[metricLabel] as number;
|
||||||
|
contributionSum += contribution;
|
||||||
|
otherRows.push(datum);
|
||||||
|
otherTooltipData.push([
|
||||||
|
extractGroupbyLabel({
|
||||||
|
datum,
|
||||||
|
groupby: groupbyLabels,
|
||||||
|
coltypeMapping,
|
||||||
|
timeFormatter: getTimeFormatter(dateFormat),
|
||||||
|
}),
|
||||||
|
numberFormatter(datum[metricLabel] as number),
|
||||||
|
percentFormatter(contribution),
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
const otherName = t('Other');
|
||||||
|
otherTooltipData.push([
|
||||||
|
t('Total'),
|
||||||
|
numberFormatter(otherSum),
|
||||||
|
percentFormatter(contributionSum),
|
||||||
|
]);
|
||||||
|
if (otherSum) {
|
||||||
|
otherDatum = {
|
||||||
|
name: otherName,
|
||||||
|
value: otherSum,
|
||||||
|
itemStyle: {
|
||||||
|
color: theme.colors.grayscale.dark1,
|
||||||
|
opacity:
|
||||||
|
filterState.selectedValues &&
|
||||||
|
!filterState.selectedValues.includes(otherName)
|
||||||
|
? OpacityEnum.SemiTransparent
|
||||||
|
: OpacityEnum.NonTransparent,
|
||||||
|
},
|
||||||
|
isOther: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const labelMap = data.reduce((acc: Record<string, string[]>, datum) => {
|
const labelMap = data.reduce((acc: Record<string, string[]>, datum) => {
|
||||||
const label = extractGroupbyLabel({
|
const label = extractGroupbyLabel({
|
||||||
datum,
|
datum,
|
||||||
@@ -192,13 +247,6 @@ export default function transformProps(
|
|||||||
|
|
||||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||||
const numberFormatter = getValueFormatter(
|
|
||||||
metric,
|
|
||||||
currencyFormats,
|
|
||||||
columnFormats,
|
|
||||||
numberFormat,
|
|
||||||
currencyFormat,
|
|
||||||
);
|
|
||||||
|
|
||||||
let totalValue = 0;
|
let totalValue = 0;
|
||||||
|
|
||||||
@@ -229,6 +277,10 @@ export default function transformProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
if (otherDatum) {
|
||||||
|
transformedData.push(otherDatum);
|
||||||
|
totalValue += otherSum;
|
||||||
|
}
|
||||||
|
|
||||||
const selectedValues = (filterState.selectedValues || []).reduce(
|
const selectedValues = (filterState.selectedValues || []).reduce(
|
||||||
(acc: Record<string, number>, selectedValue: string) => {
|
(acc: Record<string, number>, selectedValue: string) => {
|
||||||
@@ -372,6 +424,9 @@ export default function transformProps(
|
|||||||
numberFormatter,
|
numberFormatter,
|
||||||
sanitizeName: true,
|
sanitizeName: true,
|
||||||
});
|
});
|
||||||
|
if (params?.data?.isOther) {
|
||||||
|
return tooltipHtml(otherTooltipData, name);
|
||||||
|
}
|
||||||
return tooltipHtml(
|
return tooltipHtml(
|
||||||
[[metricLabel, formattedValue, formattedPercent]],
|
[[metricLabel, formattedValue, formattedPercent]],
|
||||||
name,
|
name,
|
||||||
@@ -380,7 +435,7 @@ export default function transformProps(
|
|||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...getLegendProps(legendType, legendOrientation, showLegend, theme),
|
...getLegendProps(legendType, legendOrientation, showLegend, theme),
|
||||||
data: keys,
|
data: transformedData.map(datum => datum.name),
|
||||||
},
|
},
|
||||||
graphic: showTotal
|
graphic: showTotal
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export type EchartsPieFormData = QueryFormData &
|
|||||||
dateFormat: string;
|
dateFormat: string;
|
||||||
showLabelsThreshold: number;
|
showLabelsThreshold: number;
|
||||||
roseType: 'radius' | 'area' | null;
|
roseType: 'radius' | 'area' | null;
|
||||||
|
thresholdForOther: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum EchartsPieLabelType {
|
export enum EchartsPieLabelType {
|
||||||
@@ -82,9 +83,20 @@ export const DEFAULT_FORM_DATA: EchartsPieFormData = {
|
|||||||
showLabelsThreshold: 5,
|
showLabelsThreshold: 5,
|
||||||
dateFormat: 'smart_date',
|
dateFormat: 'smart_date',
|
||||||
roseType: null,
|
roseType: null,
|
||||||
|
thresholdForOther: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PieChartTransformedProps =
|
export type PieChartTransformedProps =
|
||||||
BaseTransformedProps<EchartsPieFormData> &
|
BaseTransformedProps<EchartsPieFormData> &
|
||||||
ContextMenuTransformedProps &
|
ContextMenuTransformedProps &
|
||||||
CrossFilterTransformedProps;
|
CrossFilterTransformedProps;
|
||||||
|
|
||||||
|
export interface PieChartDataItem {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
itemStyle: {
|
||||||
|
color: string;
|
||||||
|
opacity: number;
|
||||||
|
};
|
||||||
|
isOther?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 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 { CONTRIBUTION_SUFFIX } from './constants';
|
||||||
|
|
||||||
|
export const getContributionLabel = (metricLabel: string) =>
|
||||||
|
`${metricLabel}${CONTRIBUTION_SUFFIX}`;
|
||||||
@@ -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('Distribution'), t('Flow')],
|
tags: [t('Directional'), t('ECharts'), t('Distribution'), t('Flow')],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
}),
|
}),
|
||||||
transformProps,
|
transformProps,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { t } from '@superset-ui/core';
|
import { JsonArray, t } from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
ControlPanelConfig,
|
ControlPanelConfig,
|
||||||
ControlPanelsContainerProps,
|
ControlPanelsContainerProps,
|
||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
DEFAULT_FORM_DATA,
|
DEFAULT_FORM_DATA,
|
||||||
TIME_SERIES_DESCRIPTION_TEXT,
|
TIME_SERIES_DESCRIPTION_TEXT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
import { StackControlsValue } from '../../../constants';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
logAxis,
|
logAxis,
|
||||||
@@ -321,6 +322,38 @@ const config: ControlPanelConfig = {
|
|||||||
['color_scheme'],
|
['color_scheme'],
|
||||||
['time_shift_color'],
|
['time_shift_color'],
|
||||||
...showValueSection,
|
...showValueSection,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'stackDimension',
|
||||||
|
config: {
|
||||||
|
type: 'SelectControl',
|
||||||
|
label: t('Split stack by'),
|
||||||
|
visibility: ({ controls }) =>
|
||||||
|
controls?.stack?.value === StackControlsValue.Stack,
|
||||||
|
renderTrigger: true,
|
||||||
|
description: t(
|
||||||
|
'Stack in groups, where each group corresponds to a dimension',
|
||||||
|
),
|
||||||
|
shouldMapStateToProps: (
|
||||||
|
prevState,
|
||||||
|
state,
|
||||||
|
controlState,
|
||||||
|
chartState,
|
||||||
|
) => true,
|
||||||
|
mapStateToProps: (state, controlState, chartState) => {
|
||||||
|
const value: JsonArray = state.controls.groupby
|
||||||
|
.value as JsonArray;
|
||||||
|
const valueAsStringArr: string[][] = value.map(v => {
|
||||||
|
if (v) return [v.toString(), v.toString()];
|
||||||
|
return ['', ''];
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
choices: valueAsStringArr,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
[minorTicks],
|
[minorTicks],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ 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'),
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export default function transformProps(
|
|||||||
yAxisTitleMargin,
|
yAxisTitleMargin,
|
||||||
yAxisTitlePosition,
|
yAxisTitlePosition,
|
||||||
zoomable,
|
zoomable,
|
||||||
|
stackDimension,
|
||||||
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
||||||
const refs: Refs = {};
|
const refs: Refs = {};
|
||||||
const groupBy = ensureIsArray(groupby);
|
const groupBy = ensureIsArray(groupby);
|
||||||
@@ -418,6 +419,23 @@ export default function transformProps(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
stack === StackControlsValue.Stack &&
|
||||||
|
stackDimension &&
|
||||||
|
chartProps.rawFormData.groupby
|
||||||
|
) {
|
||||||
|
const idxSelectedDimension =
|
||||||
|
formData.metrics.length > 1
|
||||||
|
? 1
|
||||||
|
: 0 + chartProps.rawFormData.groupby.indexOf(stackDimension);
|
||||||
|
for (const s of series) {
|
||||||
|
if (s.id) {
|
||||||
|
const columnsArr = labelMap[s.id];
|
||||||
|
(s as any).stack = columnsArr[idxSelectedDimension];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// axis bounds need to be parsed to replace incompatible values with undefined
|
// axis bounds need to be parsed to replace incompatible values with undefined
|
||||||
const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound);
|
const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound);
|
||||||
let [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound);
|
let [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound);
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
|
|||||||
rowLimit: number;
|
rowLimit: number;
|
||||||
seriesType: EchartsTimeseriesSeriesType;
|
seriesType: EchartsTimeseriesSeriesType;
|
||||||
stack: StackType;
|
stack: StackType;
|
||||||
|
stackDimension: string;
|
||||||
timeCompare?: string[];
|
timeCompare?: string[];
|
||||||
tooltipTimeFormat?: string;
|
tooltipTimeFormat?: string;
|
||||||
showTooltipTotal?: boolean;
|
showTooltipTotal?: boolean;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import type {
|
|||||||
CallbackDataParams,
|
CallbackDataParams,
|
||||||
} from 'echarts/types/src/util/types';
|
} from 'echarts/types/src/util/types';
|
||||||
import transformProps, { parseParams } from '../../src/Pie/transformProps';
|
import transformProps, { parseParams } from '../../src/Pie/transformProps';
|
||||||
import { EchartsPieChartProps } from '../../src/Pie/types';
|
import { EchartsPieChartProps, PieChartDataItem } from '../../src/Pie/types';
|
||||||
|
|
||||||
describe('Pie transformProps', () => {
|
describe('Pie transformProps', () => {
|
||||||
const formData: SqlaFormData = {
|
const formData: SqlaFormData = {
|
||||||
@@ -46,8 +46,13 @@ describe('Pie transformProps', () => {
|
|||||||
queriesData: [
|
queriesData: [
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
{ foo: 'Sylvester', bar: 1, sum__num: 10 },
|
{
|
||||||
{ foo: 'Arnold', bar: 2, sum__num: 2.5 },
|
foo: 'Sylvester',
|
||||||
|
bar: 1,
|
||||||
|
sum__num: 10,
|
||||||
|
sum__num__contribution: 0.8,
|
||||||
|
},
|
||||||
|
{ foo: 'Arnold', bar: 2, sum__num: 2.5, sum__num__contribution: 0.2 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -215,3 +220,77 @@ describe('Pie label string template', () => {
|
|||||||
).toEqual('Tablet:123456\n55.5');
|
).toEqual('Tablet:123456\n55.5');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Other category', () => {
|
||||||
|
const defaultFormData: SqlaFormData = {
|
||||||
|
colorScheme: 'bnbColors',
|
||||||
|
datasource: '3__table',
|
||||||
|
granularity_sqla: 'ds',
|
||||||
|
metric: 'metric',
|
||||||
|
groupby: ['foo', 'bar'],
|
||||||
|
viz_type: 'my_viz',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChartProps = (formData: Partial<SqlaFormData>) =>
|
||||||
|
new ChartProps({
|
||||||
|
formData: {
|
||||||
|
...defaultFormData,
|
||||||
|
...formData,
|
||||||
|
},
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
queriesData: [
|
||||||
|
{
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
foo: 'foo 1',
|
||||||
|
bar: 'bar 1',
|
||||||
|
metric: 1,
|
||||||
|
metric__contribution: 1 / 15, // 6.7%
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foo: 'foo 2',
|
||||||
|
bar: 'bar 2',
|
||||||
|
metric: 2,
|
||||||
|
metric__contribution: 2 / 15, // 13.3%
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foo: 'foo 3',
|
||||||
|
bar: 'bar 3',
|
||||||
|
metric: 3,
|
||||||
|
metric__contribution: 3 / 15, // 20%
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foo: 'foo 4',
|
||||||
|
bar: 'bar 4',
|
||||||
|
metric: 4,
|
||||||
|
metric__contribution: 4 / 15, // 26.7%
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foo: 'foo 5',
|
||||||
|
bar: 'bar 5',
|
||||||
|
metric: 5,
|
||||||
|
metric__contribution: 5 / 15, // 33.3%
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
theme: supersetTheme,
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates Other category', () => {
|
||||||
|
const chartProps = getChartProps({
|
||||||
|
threshold_for_other: 20,
|
||||||
|
});
|
||||||
|
const transformed = transformProps(chartProps as EchartsPieChartProps);
|
||||||
|
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||||
|
const data = series[0].data as PieChartDataItem[];
|
||||||
|
expect(data).toHaveLength(4);
|
||||||
|
expect(data[0].value).toBe(3);
|
||||||
|
expect(data[1].value).toBe(4);
|
||||||
|
expect(data[2].value).toBe(5);
|
||||||
|
expect(data[3].value).toBe(1 + 2);
|
||||||
|
expect(data[3].name).toBe('Other');
|
||||||
|
expect(data[3].isOther).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
* 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,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
DragEvent,
|
DragEvent,
|
||||||
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -39,8 +41,9 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
} from 'react-table';
|
} from 'react-table';
|
||||||
import { matchSorter, rankings } from 'match-sorter';
|
import { matchSorter, rankings } from 'match-sorter';
|
||||||
import { typedMemo, usePrevious } from '@superset-ui/core';
|
import { styled, 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,
|
||||||
@@ -50,6 +53,8 @@ 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;
|
||||||
@@ -62,7 +67,12 @@ 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: { pageSize?: number; currentPage?: number };
|
serverPaginationData: {
|
||||||
|
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;
|
||||||
@@ -71,6 +81,14 @@ 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> {
|
||||||
@@ -81,6 +99,24 @@ 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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
// 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,
|
||||||
@@ -105,6 +141,14 @@ 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>[] = [
|
||||||
@@ -115,6 +159,7 @@ 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;
|
||||||
@@ -127,7 +172,8 @@ 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);
|
||||||
@@ -188,7 +234,13 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
wrapStickyTable,
|
wrapStickyTable,
|
||||||
setColumnOrder,
|
setColumnOrder,
|
||||||
allColumns,
|
allColumns,
|
||||||
state: { pageIndex, pageSize, globalFilter: filterValue, sticky = {} },
|
state: {
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
globalFilter: filterValue,
|
||||||
|
sticky = {},
|
||||||
|
sortBy,
|
||||||
|
},
|
||||||
} = useTable<D>(
|
} = useTable<D>(
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
@@ -198,10 +250,46 @@ 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) {
|
||||||
@@ -355,6 +443,7 @@ 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}
|
||||||
@@ -362,9 +451,9 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
>
|
>
|
||||||
{hasGlobalControl ? (
|
{hasGlobalControl ? (
|
||||||
<div ref={globalControlRef} className="form-inline dt-controls">
|
<div ref={globalControlRef} className="form-inline dt-controls">
|
||||||
<div className="row">
|
<StyledRow className="row">
|
||||||
<div
|
<div
|
||||||
className={renderTimeComparisonDropdown ? 'col-sm-5' : 'col-sm-6'}
|
className={renderTimeComparisonDropdown ? 'col-sm-4' : 'col-sm-5'}
|
||||||
>
|
>
|
||||||
{hasPagination ? (
|
{hasPagination ? (
|
||||||
<SelectPageSize
|
<SelectPageSize
|
||||||
@@ -381,16 +470,35 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{searchInput ? (
|
{searchInput ? (
|
||||||
<div className="col-sm-6">
|
<StyledSpace
|
||||||
|
className={
|
||||||
|
renderTimeComparisonDropdown ? 'col-sm-7' : 'col-sm-8'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{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={
|
||||||
filterValue={filterValue}
|
manualSearch ? handleSearchChange : setGlobalFilter
|
||||||
|
}
|
||||||
|
filterValue={manualSearch ? initialSearchText : filterValue}
|
||||||
|
id={searchInputId}
|
||||||
|
serverPagination={!!serverPagination}
|
||||||
|
rowCount={rowCount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</StyledSpace>
|
||||||
) : null}
|
) : null}
|
||||||
{renderTimeComparisonDropdown ? (
|
{renderTimeComparisonDropdown ? (
|
||||||
<div
|
<div
|
||||||
@@ -400,7 +508,7 @@ export default typedMemo(function DataTable<D extends object>({
|
|||||||
{renderTimeComparisonDropdown()}
|
{renderTimeComparisonDropdown()}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</StyledRow>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()}
|
{wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()}
|
||||||
|
|||||||
@@ -16,7 +16,13 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { memo, ComponentType, ChangeEventHandler } from 'react';
|
import {
|
||||||
|
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';
|
||||||
|
|
||||||
@@ -24,8 +30,12 @@ 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
|
||||||
@@ -33,17 +43,28 @@ 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({ count, value, onChange }: SearchInputProps) {
|
function DefaultSearchInput({
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
@@ -56,8 +77,13 @@ 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 = preGlobalFilteredRows.length;
|
const count = serverPagination ? rowCount : preGlobalFilteredRows.length;
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [value, setValue] = useAsyncState(
|
const [value, setValue] = useAsyncState(
|
||||||
filterValue,
|
filterValue,
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
@@ -66,17 +92,37 @@ 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}
|
||||||
onChange={e => {
|
inputRef={inputRef}
|
||||||
const target = e.target as HTMLInputElement;
|
onChange={handleChange}
|
||||||
e.preventDefault();
|
onBlur={handleBlur}
|
||||||
setValue(target.value);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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,3 +115,11 @@ 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,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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 = () => {},
|
||||||
@@ -30,3 +31,11 @@ export const updateExternalFormData = (
|
|||||||
pageSize,
|
pageSize,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateTableOwnState = (
|
||||||
|
setDataMask: SetDataMaskHook = () => {},
|
||||||
|
modifiedOwnState: TableOwnState,
|
||||||
|
) =>
|
||||||
|
setDataMask({
|
||||||
|
ownState: modifiedOwnState,
|
||||||
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
KeyboardEvent as ReactKeyboardEvent,
|
KeyboardEvent as ReactKeyboardEvent,
|
||||||
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -61,10 +62,12 @@ import {
|
|||||||
PlusCircleOutlined,
|
PlusCircleOutlined,
|
||||||
TableOutlined,
|
TableOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { isEmpty } from 'lodash';
|
import { debounce, isEmpty, isEqual } from 'lodash';
|
||||||
import {
|
import {
|
||||||
ColorSchemeEnum,
|
ColorSchemeEnum,
|
||||||
DataColumnMeta,
|
DataColumnMeta,
|
||||||
|
SearchOption,
|
||||||
|
SortByItem,
|
||||||
TableChartTransformedProps,
|
TableChartTransformedProps,
|
||||||
} from './types';
|
} from './types';
|
||||||
import DataTable, {
|
import DataTable, {
|
||||||
@@ -76,8 +79,8 @@ 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, SERVER_PAGE_SIZE_OPTIONS } from './consts';
|
||||||
import { updateExternalFormData } from './DataTable/utils/externalAPIs';
|
import { updateTableOwnState } from './DataTable/utils/externalAPIs';
|
||||||
import getScrollBarSize from './DataTable/utils/getScrollBarSize';
|
import getScrollBarSize from './DataTable/utils/getScrollBarSize';
|
||||||
|
|
||||||
type ValueRange = [number, number];
|
type ValueRange = [number, number];
|
||||||
@@ -176,20 +179,26 @@ function SortIcon<D extends object>({ column }: { column: ColumnInstance<D> }) {
|
|||||||
return sortIcon;
|
return sortIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchInput({ count, value, onChange }: SearchInputProps) {
|
const SearchInput = ({
|
||||||
return (
|
count,
|
||||||
<span className="dt-global-filter">
|
value,
|
||||||
{t('Search')}{' '}
|
onChange,
|
||||||
<input
|
onBlur,
|
||||||
aria-label={t('Search %s records', count)}
|
inputRef,
|
||||||
className="form-control input-sm"
|
}: SearchInputProps) => (
|
||||||
placeholder={tn('search.num_records', count)}
|
<span className="dt-global-filter">
|
||||||
value={value}
|
{t('Search')}{' '}
|
||||||
onChange={onChange}
|
<input
|
||||||
/>
|
ref={inputRef}
|
||||||
</span>
|
aria-label={t('Search %s records', count)}
|
||||||
);
|
className="form-control input-sm"
|
||||||
}
|
placeholder={tn('search.num_records', count)}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
function SelectPageSize({
|
function SelectPageSize({
|
||||||
options,
|
options,
|
||||||
@@ -267,6 +276,9 @@ 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') },
|
||||||
@@ -294,7 +306,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||||||
// only take relevant page size options
|
// only take relevant page size options
|
||||||
const pageSizeOptions = useMemo(() => {
|
const pageSizeOptions = useMemo(() => {
|
||||||
const getServerPagination = (n: number) => n <= rowCount;
|
const getServerPagination = (n: number) => n <= rowCount;
|
||||||
return PAGE_SIZE_OPTIONS.filter(([n]) =>
|
return (
|
||||||
|
serverPagination ? SERVER_PAGE_SIZE_OPTIONS : PAGE_SIZE_OPTIONS
|
||||||
|
).filter(([n]) =>
|
||||||
serverPagination ? getServerPagination(n) : n <= 2 * data.length,
|
serverPagination ? getServerPagination(n) : n <= 2 * data.length,
|
||||||
) as SizeOption[];
|
) as SizeOption[];
|
||||||
}, [data.length, rowCount, serverPagination]);
|
}, [data.length, rowCount, serverPagination]);
|
||||||
@@ -679,7 +693,12 @@ 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,
|
||||||
@@ -766,6 +785,7 @@ 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);
|
||||||
@@ -1058,13 +1078,50 @@ 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) => {
|
||||||
updateExternalFormData(setDataMask, pageNumber, pageSize);
|
const modifiedOwnState = {
|
||||||
|
...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 });
|
||||||
@@ -1100,6 +1157,42 @@ 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>
|
||||||
@@ -1115,6 +1208,9 @@ 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}
|
||||||
@@ -1128,6 +1224,11 @@ 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,6 +22,7 @@ import {
|
|||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
getMetricLabel,
|
getMetricLabel,
|
||||||
isPhysicalColumn,
|
isPhysicalColumn,
|
||||||
|
QueryFormOrderBy,
|
||||||
QueryMode,
|
QueryMode,
|
||||||
QueryObject,
|
QueryObject,
|
||||||
removeDuplicates,
|
removeDuplicates,
|
||||||
@@ -34,7 +35,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 { updateExternalFormData } from './DataTable/utils/externalAPIs';
|
import { updateTableOwnState } 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,
|
||||||
@@ -191,18 +192,40 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
|||||||
|
|
||||||
const moreProps: Partial<QueryObject> = {};
|
const moreProps: Partial<QueryObject> = {};
|
||||||
const ownState = options?.ownState ?? {};
|
const ownState = options?.ownState ?? {};
|
||||||
if (formDataCopy.server_pagination) {
|
// Build Query flag to check if its for either download as csv, excel or json
|
||||||
moreProps.row_limit =
|
const isDownloadQuery =
|
||||||
ownState.pageSize ?? formDataCopy.server_page_length;
|
['csv', 'xlsx'].includes(formData?.result_format || '') ||
|
||||||
moreProps.row_offset =
|
(formData?.result_format === 'json' &&
|
||||||
(ownState.currentPage ?? 0) * (ownState.pageSize ?? 0);
|
formData?.result_type === 'results');
|
||||||
|
|
||||||
|
if (isDownloadQuery) {
|
||||||
|
moreProps.row_limit = Number(formDataCopy.row_limit) || 0;
|
||||||
|
moreProps.row_offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDownloadQuery && formDataCopy.server_pagination) {
|
||||||
|
const pageSize = ownState.pageSize ?? formDataCopy.server_page_length;
|
||||||
|
const currentPage = ownState.currentPage ?? 0;
|
||||||
|
|
||||||
|
moreProps.row_limit = pageSize;
|
||||||
|
moreProps.row_offset = currentPage * pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getting sort by in case of server pagination from own state
|
||||||
|
let sortByFromOwnState: QueryFormOrderBy[] | undefined;
|
||||||
|
if (Array.isArray(ownState?.sortBy) && ownState?.sortBy.length > 0) {
|
||||||
|
const sortByItem = ownState?.sortBy[0];
|
||||||
|
sortByFromOwnState = [[sortByItem?.key, !sortByItem?.desc]];
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -216,11 +239,12 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
|||||||
JSON.stringify(queryObject.filters)
|
JSON.stringify(queryObject.filters)
|
||||||
) {
|
) {
|
||||||
queryObject = { ...queryObject, row_offset: 0 };
|
queryObject = { ...queryObject, row_offset: 0 };
|
||||||
updateExternalFormData(
|
const modifiedOwnState = {
|
||||||
options?.hooks?.setDataMask,
|
...(options?.ownState || {}),
|
||||||
0,
|
currentPage: 0,
|
||||||
queryObject.row_limit ?? 0,
|
pageSize: 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({
|
||||||
@@ -252,12 +276,32 @@ 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: 0,
|
row_limit: Number(formData?.row_limit) ?? 0,
|
||||||
row_offset: 0,
|
row_offset: 0,
|
||||||
post_processing: [],
|
post_processing: [],
|
||||||
is_rowcount: true,
|
is_rowcount: true,
|
||||||
|
|||||||
@@ -30,3 +30,7 @@ export const PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
|
|||||||
100,
|
100,
|
||||||
200,
|
200,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const SERVER_PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
|
||||||
|
10, 20, 50, 100, 200,
|
||||||
|
]);
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ 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,
|
||||||
@@ -40,15 +43,18 @@ 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';
|
||||||
import { PAGE_SIZE_OPTIONS } from './consts';
|
import { PAGE_SIZE_OPTIONS, SERVER_PAGE_SIZE_OPTIONS } from './consts';
|
||||||
import { ColorSchemeEnum } from './types';
|
import { ColorSchemeEnum } from './types';
|
||||||
|
|
||||||
function getQueryMode(controls: ControlStateMapping): QueryMode {
|
function getQueryMode(controls: ControlStateMapping): QueryMode {
|
||||||
@@ -188,6 +194,15 @@ 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: [
|
||||||
{
|
{
|
||||||
@@ -328,6 +343,26 @@ const config: ControlPanelConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'order_desc',
|
||||||
|
config: {
|
||||||
|
type: 'CheckboxControl',
|
||||||
|
label: t('Sort descending'),
|
||||||
|
default: true,
|
||||||
|
description: t(
|
||||||
|
'If enabled, this control sorts the results/values descending, otherwise it sorts the results ascending.',
|
||||||
|
),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) => {
|
||||||
|
const hasSortMetric = Boolean(
|
||||||
|
controls?.timeseries_limit_metric?.value,
|
||||||
|
);
|
||||||
|
return hasSortMetric && isAggMode({ controls });
|
||||||
|
},
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: 'server_pagination',
|
name: 'server_pagination',
|
||||||
@@ -342,14 +377,6 @@ 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: {
|
||||||
@@ -357,7 +384,7 @@ const config: ControlPanelConfig = {
|
|||||||
freeForm: true,
|
freeForm: true,
|
||||||
label: t('Server Page Length'),
|
label: t('Server Page Length'),
|
||||||
default: 10,
|
default: 10,
|
||||||
choices: PAGE_SIZE_OPTIONS,
|
choices: SERVER_PAGE_SIZE_OPTIONS,
|
||||||
description: t('Rows per page, 0 means no pagination'),
|
description: t('Rows per page, 0 means no pagination'),
|
||||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
Boolean(controls?.server_pagination?.value),
|
Boolean(controls?.server_pagination?.value),
|
||||||
@@ -366,16 +393,43 @@ const config: ControlPanelConfig = {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: 'order_desc',
|
name: 'row_limit',
|
||||||
config: {
|
config: {
|
||||||
type: 'CheckboxControl',
|
type: 'SelectControl',
|
||||||
label: t('Sort descending'),
|
freeForm: true,
|
||||||
default: 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,
|
||||||
|
state?.maxValue || DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Re run the validations when this control value
|
||||||
|
validationDependancies: ['server_pagination'],
|
||||||
|
default: 10000,
|
||||||
|
choices: formatSelectOptions(ROW_LIMIT_OPTIONS_TABLE),
|
||||||
description: t(
|
description: t(
|
||||||
'If enabled, this control sorts the results/values descending, otherwise it sorts the results ascending.',
|
'Limits the number of the rows that are computed in the query that is the source of the data used for this chart.',
|
||||||
),
|
),
|
||||||
visibility: isAggMode,
|
},
|
||||||
resetOnHide: false,
|
override: {
|
||||||
|
default: 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ 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,
|
||||||
@@ -480,6 +489,7 @@ 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) &&
|
||||||
@@ -675,6 +685,26 @@ 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,
|
||||||
@@ -682,7 +712,10 @@ const transformProps = (
|
|||||||
isRawRecords: queryMode === QueryMode.Raw,
|
isRawRecords: queryMode === QueryMode.Raw,
|
||||||
data: passedData,
|
data: passedData,
|
||||||
totals,
|
totals,
|
||||||
columns: passedColumns,
|
columns:
|
||||||
|
Array.isArray(passedColumns) && passedColumns?.length > 0
|
||||||
|
? passedColumns
|
||||||
|
: cachedValues?.passedColumns || [],
|
||||||
serverPagination,
|
serverPagination,
|
||||||
metrics,
|
metrics,
|
||||||
percentMetrics,
|
percentMetrics,
|
||||||
@@ -697,7 +730,9 @@ const transformProps = (
|
|||||||
includeSearch,
|
includeSearch,
|
||||||
rowCount,
|
rowCount,
|
||||||
pageSize: serverPagination
|
pageSize: serverPagination
|
||||||
? serverPageLength
|
? serverPaginationData?.pageSize
|
||||||
|
? serverPaginationData?.pageSize
|
||||||
|
: serverPageLength
|
||||||
: getPageSize(pageLength, data.length, columns.length),
|
: getPageSize(pageLength, data.length, columns.length),
|
||||||
filters: filterState.filters,
|
filters: filterState.filters,
|
||||||
emitCrossFilters,
|
emitCrossFilters,
|
||||||
@@ -711,6 +746,9 @@ const transformProps = (
|
|||||||
basicColorFormatters,
|
basicColorFormatters,
|
||||||
startDateOffset,
|
startDateOffset,
|
||||||
basicColorColumnFormatters,
|
basicColorColumnFormatters,
|
||||||
|
hasServerPageLengthChanged,
|
||||||
|
serverPageLength,
|
||||||
|
slice_id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -114,13 +114,32 @@ 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: { pageSize?: number; currentPage?: number };
|
serverPaginationData: ServerPaginationData;
|
||||||
setDataMask: SetDataMaskHook;
|
setDataMask: SetDataMaskHook;
|
||||||
isRawRecords?: boolean;
|
isRawRecords?: boolean;
|
||||||
data: D[];
|
data: D[];
|
||||||
@@ -152,6 +171,11 @@ 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 {
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ const SqlEditor: FC<Props> = ({
|
|||||||
|
|
||||||
const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form');
|
const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form');
|
||||||
|
|
||||||
|
const isTempId = (value: unknown): boolean => Number.isNaN(Number(value));
|
||||||
|
|
||||||
const startQuery = useCallback(
|
const startQuery = useCallback(
|
||||||
(ctasArg = false, ctas_method = CtasEnum.Table) => {
|
(ctasArg = false, ctas_method = CtasEnum.Table) => {
|
||||||
if (!database) {
|
if (!database) {
|
||||||
@@ -915,7 +917,7 @@ const SqlEditor: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<AceEditorWrapper
|
<AceEditorWrapper
|
||||||
autocomplete={autocompleteEnabled}
|
autocomplete={autocompleteEnabled && !isTempId(queryEditor.id)}
|
||||||
onBlur={onSqlChanged}
|
onBlur={onSqlChanged}
|
||||||
onChange={onSqlChanged}
|
onChange={onSqlChanged}
|
||||||
queryEditorId={queryEditor.id}
|
queryEditorId={queryEditor.id}
|
||||||
|
|||||||
@@ -77,21 +77,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.caret {
|
|
||||||
border: none;
|
|
||||||
color: @gray;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: @gray-darker;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
font-family: 'FontAwesome';
|
|
||||||
font-size: @font-size-xs;
|
|
||||||
content: '\f078';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typography =================================================================
|
// Typography =================================================================
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -11,37 +11,23 @@
|
|||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing,
|
* Unless required by applicable law or agreed to in writing,
|
||||||
* software distributed under the License is distributed on an
|
* software distributed under the License is distributed on an
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||||
* KIND, either express or implied. See the License for the
|
* OF ANY KIND, either express or implied. See the License for
|
||||||
* specific language governing permissions and limitations
|
* the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import { getTooltipHTML } from './Tooltip';
|
||||||
import Tooltip, { getTooltipHTML } from './Tooltip';
|
|
||||||
|
|
||||||
test('should render a tooltip', () => {
|
test('getTooltipHTML returns the expected HTML (string inputs)', () => {
|
||||||
const expected = {
|
|
||||||
title: 'tooltip title',
|
|
||||||
icon: <div>icon</div>,
|
|
||||||
body: <div>body</div>,
|
|
||||||
meta: 'meta',
|
|
||||||
footer: <div>footer</div>,
|
|
||||||
};
|
|
||||||
render(<Tooltip {...expected} />);
|
|
||||||
expect(screen.getByText(expected.title)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(expected.meta)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('icon')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('body')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns the tooltip HTML', () => {
|
|
||||||
const html = getTooltipHTML({
|
const html = getTooltipHTML({
|
||||||
title: 'tooltip title',
|
title: 'tooltip title',
|
||||||
icon: <div>icon</div>,
|
body: 'body text',
|
||||||
body: <div>body</div>,
|
footer: 'footer note',
|
||||||
meta: 'meta',
|
|
||||||
footer: <div>footer</div>,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(html).toContain('tooltip-detail');
|
||||||
expect(html).toContain('tooltip title');
|
expect(html).toContain('tooltip title');
|
||||||
|
expect(html).toContain('body text');
|
||||||
|
expect(html).toContain('footer note');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,42 +16,22 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { renderToStaticMarkup } from 'react-dom/server';
|
|
||||||
import { Tag } from 'src/components';
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title?: string;
|
||||||
icon?: React.ReactNode;
|
body?: string;
|
||||||
body?: React.ReactNode;
|
footer?: string;
|
||||||
meta?: string;
|
|
||||||
footer?: React.ReactNode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Tooltip: React.FC<Props> = ({
|
export function getTooltipHTML({ title, body, footer }: Props): string {
|
||||||
title,
|
const html = `
|
||||||
icon,
|
<div class="tooltip-detail">
|
||||||
body,
|
${title ? `<div class="tooltip-detail-title">${title}</div>` : ''}
|
||||||
meta,
|
${body ? `<div class="tooltip-detail-body">${body}</div>` : ''}
|
||||||
footer,
|
${footer ? `<div class="tooltip-detail-footer">${footer}</div>` : ''}
|
||||||
}) => (
|
|
||||||
<div className="tooltip-detail">
|
|
||||||
<div className="tooltip-detail-head">
|
|
||||||
<div className="tooltip-detail-title">
|
|
||||||
{icon}
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
{meta && (
|
|
||||||
<span className="tooltip-detail-meta">
|
|
||||||
<Tag color="default">{meta}</Tag>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{body && <div className="tooltip-detail-body">{body ?? title}</div>}
|
`;
|
||||||
{footer && <div className="tooltip-detail-footer">{footer}</div>}
|
return DOMPurify.sanitize(html);
|
||||||
</div>
|
}
|
||||||
);
|
|
||||||
|
|
||||||
export const getTooltipHTML = (props: Props) =>
|
|
||||||
`${renderToStaticMarkup(<Tooltip {...props} />)}`;
|
|
||||||
|
|
||||||
export default Tooltip;
|
|
||||||
|
|||||||
@@ -190,50 +190,37 @@ export default function AsyncAceEditor(
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Global
|
<Global
|
||||||
|
key="ace-tooltip-global"
|
||||||
styles={css`
|
styles={css`
|
||||||
.ace_tooltip {
|
.ace_tooltip {
|
||||||
margin-left: ${supersetTheme.gridUnit * 2}px;
|
all: unset;
|
||||||
padding: 0px;
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
background: ${supersetTheme.colors.grayscale.light5};
|
||||||
border: 1px solid ${supersetTheme.colors.grayscale.light1};
|
border: 1px solid ${supersetTheme.colors.grayscale.light1};
|
||||||
|
padding: ${supersetTheme.gridUnit}px
|
||||||
|
${supersetTheme.gridUnit * 2}px;
|
||||||
|
line-height: 1.4;
|
||||||
|
max-width: 400px;
|
||||||
|
min-width: 200px;
|
||||||
|
pointer-events: auto;
|
||||||
|
font-size: ${supersetTheme.typography.sizes.m}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .tooltip-detail {
|
& .tooltip-detail {
|
||||||
background-color: ${supersetTheme.colors.grayscale.light5};
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
min-width: ${supersetTheme.gridUnit * 50}px;
|
|
||||||
max-width: ${supersetTheme.gridUnit * 100}px;
|
|
||||||
& .tooltip-detail-head {
|
|
||||||
background-color: ${supersetTheme.colors.grayscale.light4};
|
|
||||||
color: ${supersetTheme.colors.grayscale.dark1};
|
|
||||||
display: flex;
|
|
||||||
column-gap: ${supersetTheme.gridUnit}px;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
& .tooltip-detail-title {
|
& .tooltip-detail-title {
|
||||||
display: flex;
|
font-weight: bold;
|
||||||
column-gap: ${supersetTheme.gridUnit}px;
|
font-size: ${supersetTheme.typography.sizes.m}px;
|
||||||
}
|
}
|
||||||
& .tooltip-detail-body {
|
& .tooltip-detail-body {
|
||||||
word-break: break-word;
|
font-size: ${supersetTheme.typography.sizes.s}px;
|
||||||
|
padding: ${supersetTheme.gridUnit}px;
|
||||||
}
|
}
|
||||||
& .tooltip-detail-head,
|
& .tooltip-detail-head,
|
||||||
& .tooltip-detail-body {
|
& .tooltip-detail-body {
|
||||||
padding: ${supersetTheme.gridUnit}px
|
|
||||||
${supersetTheme.gridUnit * 2}px;
|
|
||||||
}
|
}
|
||||||
& .tooltip-detail-footer {
|
& .tooltip-detail-footer {
|
||||||
border-top: 1px ${supersetTheme.colors.grayscale.light2}
|
font-size: ${supersetTheme.typography.sizes.s}px;
|
||||||
solid;
|
|
||||||
padding: 0 ${supersetTheme.gridUnit * 2}px;
|
|
||||||
color: ${supersetTheme.colors.grayscale.dark1};
|
|
||||||
font-size: ${supersetTheme.typography.sizes.xs}px;
|
|
||||||
}
|
|
||||||
& .tooltip-detail-meta {
|
|
||||||
& > .ant-tag {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -327,6 +327,10 @@ 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 (
|
||||||
<>
|
<>
|
||||||
@@ -367,6 +371,7 @@ 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,6 +34,7 @@ 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';
|
||||||
@@ -53,10 +54,15 @@ 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 {
|
||||||
@@ -586,6 +592,8 @@ function OwnersSelector({ datasource, onChange }) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const ResultTable =
|
||||||
|
extensionsRegistry.get('sqleditor.extension.resultTable') ?? FilterableTable;
|
||||||
|
|
||||||
class DatasourceEditor extends PureComponent {
|
class DatasourceEditor extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -698,6 +706,23 @@ 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();
|
||||||
@@ -1078,14 +1103,62 @@ class DatasourceEditor extends PureComponent {
|
|||||||
<TextAreaControl
|
<TextAreaControl
|
||||||
language="sql"
|
language="sql"
|
||||||
offerEditInModal={false}
|
offerEditInModal={false}
|
||||||
minLines={20}
|
minLines={10}
|
||||||
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>
|
||||||
@@ -1466,6 +1539,10 @@ class DatasourceEditor extends PureComponent {
|
|||||||
</DatasourceContainer>
|
</DatasourceContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.resetQuery();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DatasourceEditor.defaultProps = defaultProps;
|
DatasourceEditor.defaultProps = defaultProps;
|
||||||
@@ -1473,4 +1550,14 @@ DatasourceEditor.propTypes = propTypes;
|
|||||||
|
|
||||||
const DataSourceComponent = withTheme(DatasourceEditor);
|
const DataSourceComponent = withTheme(DatasourceEditor);
|
||||||
|
|
||||||
export default withToasts(DataSourceComponent);
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
runQuery: payload => dispatch(executeQuery(payload)),
|
||||||
|
resetQuery: () => dispatch(resetDatabaseState()),
|
||||||
|
});
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
test: state.queryApi,
|
||||||
|
database: state.database,
|
||||||
|
});
|
||||||
|
export default withToasts(
|
||||||
|
connect(mapStateToProps, mapDispatchToProps)(DataSourceComponent),
|
||||||
|
);
|
||||||
|
|||||||
@@ -120,71 +120,83 @@ 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>) => {
|
||||||
table_name: datasource.table_name,
|
const payload: Record<string, any> = {
|
||||||
database_id: datasource.database?.id,
|
table_name: datasource.table_name,
|
||||||
sql: datasource.sql,
|
database_id: datasource.database?.id,
|
||||||
filter_select_enabled: datasource.filter_select_enabled,
|
sql: datasource.sql,
|
||||||
fetch_values_predicate: datasource.fetch_values_predicate,
|
filter_select_enabled: datasource.filter_select_enabled,
|
||||||
schema:
|
fetch_values_predicate: datasource.fetch_values_predicate,
|
||||||
datasource.tableSelector?.schema ||
|
schema:
|
||||||
datasource.databaseSelector?.schema ||
|
datasource.tableSelector?.schema ||
|
||||||
datasource.schema,
|
datasource.databaseSelector?.schema ||
|
||||||
description: datasource.description,
|
datasource.schema,
|
||||||
main_dttm_col: datasource.main_dttm_col,
|
description: datasource.description,
|
||||||
normalize_columns: datasource.normalize_columns,
|
main_dttm_col: datasource.main_dttm_col,
|
||||||
always_filter_main_dttm: datasource.always_filter_main_dttm,
|
normalize_columns: datasource.normalize_columns,
|
||||||
offset: datasource.offset,
|
always_filter_main_dttm: datasource.always_filter_main_dttm,
|
||||||
default_endpoint: datasource.default_endpoint,
|
offset: datasource.offset,
|
||||||
cache_timeout:
|
default_endpoint: datasource.default_endpoint,
|
||||||
datasource.cache_timeout === '' ? null : datasource.cache_timeout,
|
cache_timeout:
|
||||||
is_sqllab_view: datasource.is_sqllab_view,
|
datasource.cache_timeout === '' ? null : datasource.cache_timeout,
|
||||||
template_params: datasource.template_params,
|
is_sqllab_view: datasource.is_sqllab_view,
|
||||||
extra: datasource.extra,
|
template_params: datasource.template_params,
|
||||||
is_managed_externally: datasource.is_managed_externally,
|
extra: datasource.extra,
|
||||||
external_url: datasource.external_url,
|
is_managed_externally: datasource.is_managed_externally,
|
||||||
metrics: datasource?.metrics?.map((metric: DatasetObject['metrics'][0]) => {
|
external_url: datasource.external_url,
|
||||||
const metricBody: any = {
|
metrics: datasource?.metrics?.map(
|
||||||
expression: metric.expression,
|
(metric: DatasetObject['metrics'][0]) => {
|
||||||
description: metric.description,
|
const metricBody: any = {
|
||||||
metric_name: metric.metric_name,
|
expression: metric.expression,
|
||||||
metric_type: metric.metric_type,
|
description: metric.description,
|
||||||
d3format: metric.d3format || null,
|
metric_name: metric.metric_name,
|
||||||
currency: !isDefined(metric.currency)
|
metric_type: metric.metric_type,
|
||||||
? null
|
d3format: metric.d3format || null,
|
||||||
: JSON.stringify(metric.currency),
|
currency: !isDefined(metric.currency)
|
||||||
verbose_name: metric.verbose_name,
|
? null
|
||||||
warning_text: metric.warning_text,
|
: JSON.stringify(metric.currency),
|
||||||
uuid: metric.uuid,
|
verbose_name: metric.verbose_name,
|
||||||
extra: buildExtraJsonObject(metric),
|
warning_text: metric.warning_text,
|
||||||
};
|
uuid: metric.uuid,
|
||||||
if (!Number.isNaN(Number(metric.id))) {
|
extra: buildExtraJsonObject(metric),
|
||||||
metricBody.id = metric.id;
|
};
|
||||||
}
|
if (!Number.isNaN(Number(metric.id))) {
|
||||||
return metricBody;
|
metricBody.id = metric.id;
|
||||||
}),
|
}
|
||||||
columns: datasource?.columns?.map(
|
return metricBody;
|
||||||
(column: DatasetObject['columns'][0]) => ({
|
},
|
||||||
id: typeof column.id === 'number' ? column.id : undefined,
|
),
|
||||||
column_name: column.column_name,
|
columns: datasource?.columns?.map(
|
||||||
type: column.type,
|
(column: DatasetObject['columns'][0]) => ({
|
||||||
advanced_data_type: column.advanced_data_type,
|
id: typeof column.id === 'number' ? column.id : undefined,
|
||||||
verbose_name: column.verbose_name,
|
column_name: column.column_name,
|
||||||
description: column.description,
|
type: column.type,
|
||||||
expression: column.expression,
|
advanced_data_type: column.advanced_data_type,
|
||||||
filterable: column.filterable,
|
verbose_name: column.verbose_name,
|
||||||
groupby: column.groupby,
|
description: column.description,
|
||||||
is_active: column.is_active,
|
expression: column.expression,
|
||||||
is_dttm: column.is_dttm,
|
filterable: column.filterable,
|
||||||
python_date_format: column.python_date_format || null,
|
groupby: column.groupby,
|
||||||
uuid: column.uuid,
|
is_active: column.is_active,
|
||||||
extra: buildExtraJsonObject(column),
|
is_dttm: column.is_dttm,
|
||||||
}),
|
python_date_format: column.python_date_format || null,
|
||||||
),
|
uuid: column.uuid,
|
||||||
owners: datasource.owners.map(
|
extra: buildExtraJsonObject(column),
|
||||||
(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,13 +29,20 @@ 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');
|
||||||
@@ -47,3 +54,9 @@ 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,6 +21,7 @@ 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 {
|
||||||
@@ -28,16 +29,17 @@ 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>({
|
||||||
@@ -46,9 +48,11 @@ 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 => {
|
||||||
@@ -62,32 +66,51 @@ export default function Field<V>({
|
|||||||
onChange: onControlChange,
|
onChange: onControlChange,
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<FormItem
|
<div
|
||||||
label={
|
css={
|
||||||
<FormLabel className="m-r-5">
|
additionalControl &&
|
||||||
{label || fieldKey}
|
css`
|
||||||
{compact && description && (
|
position: relative;
|
||||||
<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}
|
|
||||||
>
|
>
|
||||||
{hookedControl}
|
{additionalControl}
|
||||||
{!compact && description && (
|
<FormItem
|
||||||
|
label={
|
||||||
|
<FormLabel className="m-r-5">
|
||||||
|
{label || fieldKey}
|
||||||
|
{compact && description && (
|
||||||
|
<Tooltip id="field-descr" placement="right" title={description}>
|
||||||
|
<Icons.InfoCircleFilled iconSize="s" className="m-l-5" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
}
|
||||||
|
css={inline && formItemInlineCss}
|
||||||
|
>
|
||||||
|
{hookedControl}
|
||||||
|
{!compact && description && (
|
||||||
|
<div
|
||||||
|
css={(theme: SupersetTheme) => ({
|
||||||
|
color: theme.colors.grayscale.base,
|
||||||
|
[inline ? 'marginLeft' : 'marginTop']: theme.gridUnit,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
{errorMessage && (
|
||||||
<div
|
<div
|
||||||
css={(theme: SupersetTheme) => ({
|
css={(theme: SupersetTheme) => ({
|
||||||
color: theme.colors.grayscale.base,
|
color: theme.colors.error.base,
|
||||||
[inline ? 'marginLeft' : 'marginTop']: theme.gridUnit,
|
marginTop: -16,
|
||||||
|
fontSize: theme.typography.sizes.s,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{description}
|
{errorMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormItem>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,12 @@ import {
|
|||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
BellOutlined,
|
BellOutlined,
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
|
BulbOutlined,
|
||||||
CaretUpOutlined,
|
CaretUpOutlined,
|
||||||
CaretDownOutlined,
|
CaretDownOutlined,
|
||||||
CaretLeftOutlined,
|
CaretLeftOutlined,
|
||||||
CaretRightOutlined,
|
CaretRightOutlined,
|
||||||
|
CaretRightFilled,
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
@@ -60,6 +62,7 @@ import {
|
|||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
EyeInvisibleOutlined,
|
EyeInvisibleOutlined,
|
||||||
FallOutlined,
|
FallOutlined,
|
||||||
|
FieldNumberOutlined,
|
||||||
FieldTimeOutlined,
|
FieldTimeOutlined,
|
||||||
FileImageOutlined,
|
FileImageOutlined,
|
||||||
FileOutlined,
|
FileOutlined,
|
||||||
@@ -89,6 +92,7 @@ import {
|
|||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
|
ShareAltOutlined,
|
||||||
StarOutlined,
|
StarOutlined,
|
||||||
StarFilled,
|
StarFilled,
|
||||||
StopOutlined,
|
StopOutlined,
|
||||||
@@ -130,10 +134,12 @@ const AntdIcons = {
|
|||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
BellOutlined,
|
BellOutlined,
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
|
BulbOutlined,
|
||||||
CaretUpOutlined,
|
CaretUpOutlined,
|
||||||
CaretDownOutlined,
|
CaretDownOutlined,
|
||||||
CaretLeftOutlined,
|
CaretLeftOutlined,
|
||||||
CaretRightOutlined,
|
CaretRightOutlined,
|
||||||
|
CaretRightFilled,
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
@@ -160,6 +166,7 @@ const AntdIcons = {
|
|||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
EyeInvisibleOutlined,
|
EyeInvisibleOutlined,
|
||||||
FallOutlined,
|
FallOutlined,
|
||||||
|
FieldNumberOutlined,
|
||||||
FieldTimeOutlined,
|
FieldTimeOutlined,
|
||||||
FileImageOutlined,
|
FileImageOutlined,
|
||||||
FileOutlined,
|
FileOutlined,
|
||||||
@@ -189,6 +196,7 @@ const AntdIcons = {
|
|||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
|
ShareAltOutlined,
|
||||||
StarOutlined,
|
StarOutlined,
|
||||||
StarFilled,
|
StarFilled,
|
||||||
StopOutlined,
|
StopOutlined,
|
||||||
|
|||||||
@@ -443,6 +443,7 @@ const Select = forwardRef(
|
|||||||
<StyledBulkActionsContainer className="select-bulk-actions" size={0}>
|
<StyledBulkActionsContainer className="select-bulk-actions" size={0}>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
|
buttonStyle="link"
|
||||||
buttonSize="xsmall"
|
buttonSize="xsmall"
|
||||||
disabled={bulkSelectCounts.selectable === 0}
|
disabled={bulkSelectCounts.selectable === 0}
|
||||||
onMouseDown={e => {
|
onMouseDown={e => {
|
||||||
@@ -455,6 +456,7 @@ const Select = forwardRef(
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
|
buttonStyle="link"
|
||||||
buttonSize="xsmall"
|
buttonSize="xsmall"
|
||||||
disabled={bulkSelectCounts.deselectable === 0}
|
disabled={bulkSelectCounts.deselectable === 0}
|
||||||
onMouseDown={e => {
|
onMouseDown={e => {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ 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';
|
||||||
@@ -188,7 +189,10 @@ const Row = props => {
|
|||||||
observerDisabler = new IntersectionObserver(
|
observerDisabler = new IntersectionObserver(
|
||||||
([entry]) => {
|
([entry]) => {
|
||||||
if (!entry.isIntersecting && isComponentVisibleRef.current) {
|
if (!entry.isIntersecting && isComponentVisibleRef.current) {
|
||||||
setIsInView(false);
|
// Reference: https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-rootmargin
|
||||||
|
if (!isEmbedded()) {
|
||||||
|
setIsInView(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
26
superset-frontend/src/dashboard/util/isEmbedded.ts
Normal file
26
superset-frontend/src/dashboard/util/isEmbedded.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
68
superset-frontend/src/database/actions.ts
Normal file
68
superset-frontend/src/database/actions.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
56
superset-frontend/src/database/reducers.ts
Normal file
56
superset-frontend/src/database/reducers.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
superset-frontend/src/database/types.ts
Normal file
57
superset-frontend/src/database/types.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'src/utils/getBootstrapData';
|
import getBootstrapData, { applicationRoot } 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>
|
<Router basename={applicationRoot()}>
|
||||||
{/* 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,6 +187,7 @@ 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,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const useResultsPane = ({
|
|||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [responseError, setResponseError] = useState<string>('');
|
const [responseError, setResponseError] = useState<string>('');
|
||||||
const queryCount = metadata?.queryObjectCount ?? 1;
|
const queryCount = metadata?.queryObjectCount ?? 1;
|
||||||
|
const isQueryCountDynamic = metadata?.dynamicQueryObjectCount;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// it's an invalid formData when gets a errorMessage
|
// it's an invalid formData when gets a errorMessage
|
||||||
@@ -139,19 +140,21 @@ export const useResultsPane = ({
|
|||||||
<EmptyState image="document.svg" title={title} />,
|
<EmptyState image="document.svg" title={title} />,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return resultResp
|
const resultRespToDisplay = isQueryCountDynamic
|
||||||
.slice(0, queryCount)
|
? resultResp
|
||||||
.map((result, idx) => (
|
: resultResp.slice(0, queryCount);
|
||||||
<SingleQueryResultPane
|
|
||||||
data={result.data}
|
return resultRespToDisplay.map((result, idx) => (
|
||||||
colnames={result.colnames}
|
<SingleQueryResultPane
|
||||||
coltypes={result.coltypes}
|
data={result.data}
|
||||||
rowcount={result.rowcount}
|
colnames={result.colnames}
|
||||||
dataSize={dataSize}
|
coltypes={result.coltypes}
|
||||||
datasourceId={queryFormData.datasource}
|
rowcount={result.rowcount}
|
||||||
key={idx}
|
dataSize={dataSize}
|
||||||
isVisible={isVisible}
|
datasourceId={queryFormData.datasource}
|
||||||
canDownload={canDownload}
|
key={idx}
|
||||||
/>
|
isVisible={isVisible}
|
||||||
));
|
canDownload={canDownload}
|
||||||
|
/>
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -174,4 +174,33 @@ describe('ResultsPaneOnDashboard', () => {
|
|||||||
expect(await findByText('Results')).toBeVisible();
|
expect(await findByText('Results')).toBeVisible();
|
||||||
expect(await findByText('Results 2')).toBeVisible();
|
expect(await findByText('Results 2')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('dynamic number of results pane', async () => {
|
||||||
|
const FakeChart = () => <span>test</span>;
|
||||||
|
const metadata = new ChartMetadata({
|
||||||
|
name: 'test-chart',
|
||||||
|
thumbnail: '',
|
||||||
|
dynamicQueryObjectCount: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const plugin = new ChartPlugin({
|
||||||
|
metadata,
|
||||||
|
Chart: FakeChart,
|
||||||
|
});
|
||||||
|
plugin.configure({ key: VizType.MixedTimeseries }).register();
|
||||||
|
|
||||||
|
const props = createResultsPaneOnDashboardProps({
|
||||||
|
sliceId: 196,
|
||||||
|
vizType: VizType.MixedTimeseries,
|
||||||
|
});
|
||||||
|
const { findByText, queryByText } = render(
|
||||||
|
<ResultsPaneOnDashboard {...props} />,
|
||||||
|
{
|
||||||
|
useRedux: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(await findByText('Results')).toBeVisible();
|
||||||
|
expect(await findByText('Results 2')).toBeVisible();
|
||||||
|
expect(queryByText('Results 3')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* 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 { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||||
|
import NumberControl from '.';
|
||||||
|
|
||||||
|
const mockedProps = {
|
||||||
|
min: -5,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
default: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('render', () => {
|
||||||
|
const { container } = render(<NumberControl {...mockedProps} />);
|
||||||
|
expect(container).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type number', async () => {
|
||||||
|
const props = {
|
||||||
|
...mockedProps,
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
render(<NumberControl {...props} />);
|
||||||
|
const input = screen.getByRole('spinbutton');
|
||||||
|
await userEvent.type(input, '9');
|
||||||
|
expect(props.onChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(props.onChange).toHaveBeenLastCalledWith(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type >max', async () => {
|
||||||
|
const props = {
|
||||||
|
...mockedProps,
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
render(<NumberControl {...props} />);
|
||||||
|
const input = screen.getByRole('spinbutton');
|
||||||
|
await userEvent.type(input, '20');
|
||||||
|
expect(props.onChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(props.onChange).toHaveBeenLastCalledWith(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type NaN', async () => {
|
||||||
|
const props = {
|
||||||
|
...mockedProps,
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
render(<NumberControl {...props} />);
|
||||||
|
const input = screen.getByRole('spinbutton');
|
||||||
|
await userEvent.type(input, 'not a number');
|
||||||
|
expect(props.onChange).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 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 { styled } from '@superset-ui/core';
|
||||||
|
import { InputNumber } from 'src/components/Input';
|
||||||
|
import ControlHeader, { ControlHeaderProps } from '../../ControlHeader';
|
||||||
|
|
||||||
|
type NumberValueType = number | undefined;
|
||||||
|
|
||||||
|
export interface NumberControlProps extends ControlHeaderProps {
|
||||||
|
onChange?: (value: NumberValueType) => void;
|
||||||
|
value?: NumberValueType;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FullWidthDiv = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FullWidthInputNumber = styled(InputNumber)`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function parseValue(value: string | number | null | undefined) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const num = Number(value);
|
||||||
|
return Number.isNaN(num) ? undefined : num;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NumberControl({
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
...rest
|
||||||
|
}: NumberControlProps) {
|
||||||
|
return (
|
||||||
|
<FullWidthDiv>
|
||||||
|
<ControlHeader {...rest} />
|
||||||
|
<FullWidthInputNumber
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={value => onChange?.(parseValue(value))}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label={rest.label}
|
||||||
|
/>
|
||||||
|
</FullWidthDiv>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,10 +19,7 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { TextArea } from 'src/components/Input';
|
import { TextArea } from 'src/components/Input';
|
||||||
import {
|
import { Tooltip, TooltipProps } from 'src/components/Tooltip';
|
||||||
Tooltip,
|
|
||||||
TooltipProps as TooltipOptions,
|
|
||||||
} from 'src/components/Tooltip';
|
|
||||||
import { t, withTheme } from '@superset-ui/core';
|
import { t, withTheme } from '@superset-ui/core';
|
||||||
|
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
@@ -59,7 +56,7 @@ const propTypes = {
|
|||||||
'vertical',
|
'vertical',
|
||||||
]),
|
]),
|
||||||
textAreaStyles: PropTypes.object,
|
textAreaStyles: PropTypes.object,
|
||||||
tooltipOptions: PropTypes.oneOf([null, TooltipOptions]),
|
tooltipOptions: PropTypes.oneOf([null, TooltipProps]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import { ComparisonRangeLabel } from './ComparisonRangeLabel';
|
|||||||
import LayerConfigsControl from './LayerConfigsControl/LayerConfigsControl';
|
import LayerConfigsControl from './LayerConfigsControl/LayerConfigsControl';
|
||||||
import MapViewControl from './MapViewControl/MapViewControl';
|
import MapViewControl from './MapViewControl/MapViewControl';
|
||||||
import ZoomConfigControl from './ZoomConfigControl/ZoomConfigControl';
|
import ZoomConfigControl from './ZoomConfigControl/ZoomConfigControl';
|
||||||
|
import NumberControl from './NumberControl';
|
||||||
|
|
||||||
const controlMap = {
|
const controlMap = {
|
||||||
AnnotationLayerControl,
|
AnnotationLayerControl,
|
||||||
@@ -90,6 +91,7 @@ const controlMap = {
|
|||||||
ComparisonRangeLabel,
|
ComparisonRangeLabel,
|
||||||
TimeOffsetControl,
|
TimeOffsetControl,
|
||||||
ZoomConfigControl,
|
ZoomConfigControl,
|
||||||
|
NumberControl,
|
||||||
...sharedControlComponents,
|
...sharedControlComponents,
|
||||||
};
|
};
|
||||||
export default controlMap;
|
export default controlMap;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user