mirror of
https://github.com/apache/superset.git
synced 2026-05-04 23:44:23 +00:00
Compare commits
107 Commits
docs_opena
...
fix-long-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fca4900cd9 | ||
|
|
ca74ae75a6 | ||
|
|
ae6c072661 | ||
|
|
5f2f12d347 | ||
|
|
fc7ba060c1 | ||
|
|
3a3984006c | ||
|
|
d11b6d557e | ||
|
|
2f007bf7a5 | ||
|
|
6513445000 | ||
|
|
3ef92e5610 | ||
|
|
57bb425fb0 | ||
|
|
2fba789e8d | ||
|
|
08655a7559 | ||
|
|
3256008a59 | ||
|
|
da8efd36d7 | ||
|
|
5541dad32b | ||
|
|
b3f436a030 | ||
|
|
b00660acf1 | ||
|
|
a6af4f4d7a | ||
|
|
cc3460832f | ||
|
|
edc60914f6 | ||
|
|
c9518485ba | ||
|
|
a26e1d822a | ||
|
|
a7aa8f7cef | ||
|
|
ff34e3c81e | ||
|
|
20519158d2 | ||
|
|
cacf1e06d6 | ||
|
|
fa0c5891bf | ||
|
|
fc13a0fde5 | ||
|
|
ade85daee2 | ||
|
|
2d26af25c1 | ||
|
|
b033406387 | ||
|
|
c09f8f6f76 | ||
|
|
401ce56fa1 | ||
|
|
cf315388f2 | ||
|
|
f219dc1794 | ||
|
|
ed20d2a917 | ||
|
|
235c9d2ebf | ||
|
|
fdea4e21b0 | ||
|
|
e20a08cb14 | ||
|
|
429935a277 | ||
|
|
a4bb11c755 | ||
|
|
f0b6e87091 | ||
|
|
ea5a609d0b | ||
|
|
0abe6eed89 | ||
|
|
e205846845 | ||
|
|
deef923825 | ||
|
|
0fa3feb088 | ||
|
|
1393f7d3d2 | ||
|
|
b7ba50033a | ||
|
|
ce9759785a | ||
|
|
8de58b9848 | ||
|
|
cc8ab2c556 | ||
|
|
1409b1a25b | ||
|
|
bdfb698aa4 | ||
|
|
57183da315 | ||
|
|
c928f23e1b | ||
|
|
0c89914a6d | ||
|
|
630e0e0240 | ||
|
|
513047c3bb | ||
|
|
d932837a3c | ||
|
|
38868f9ff4 | ||
|
|
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 |
36
.coveragerc
Normal file
36
.coveragerc
Normal file
@@ -0,0 +1,36 @@
|
||||
# .coveragerc to control coverage.py
|
||||
[run]
|
||||
branch = True
|
||||
source = superset
|
||||
# omit = bad_file.py
|
||||
|
||||
[paths]
|
||||
source =
|
||||
superset/
|
||||
*/site-packages/
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma
|
||||
pragma: no cover
|
||||
|
||||
# Don't complain about missing debug-only code:
|
||||
def __repr__
|
||||
if self\.debug
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
|
||||
# Ignore importlib backport
|
||||
from importlib
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
#fail_under = 100
|
||||
show_missing = True
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -27,6 +27,8 @@ updates:
|
||||
- package-ecosystem: "uv"
|
||||
directory: "requirements/"
|
||||
open-pull-requests-limit: 10
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- uv
|
||||
- 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
|
||||
|
||||
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
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["current", "previous"]
|
||||
python-version: ["current", "previous", "next"]
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/superset-docs-verify.yml
vendored
2
.github/workflows/superset-docs-verify.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Do not bump this linkinator-action version without opening
|
||||
# an ASF Infra ticket to allow the new verison first!
|
||||
# an ASF Infra ticket to allow the new version first!
|
||||
- uses: JustinBeckwith/linkinator-action@v1.11.0
|
||||
continue-on-error: true # This will make the job advisory (non-blocking, no red X)
|
||||
with:
|
||||
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["current", "previous"]
|
||||
python-version: ["current", "previous", "next"]
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["previous", "current"]
|
||||
python-version: ["previous", "current", "next"]
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
steps:
|
||||
@@ -45,6 +45,13 @@ jobs:
|
||||
SUPERSET_SECRET_KEY: not-a-secret
|
||||
run: |
|
||||
pytest --durations-min=0.5 --cov-report= --cov=superset ./tests/common ./tests/unit_tests --cache-clear --maxfail=50
|
||||
- name: Python 100% coverage unit tests
|
||||
if: steps.check.outputs.python
|
||||
env:
|
||||
SUPERSET_TESTENV: true
|
||||
SUPERSET_SECRET_KEY: not-a-secret
|
||||
run: |
|
||||
pytest --durations-min=0.5 --cov-report= --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
######################################################################
|
||||
# Node stage to deal with static asset construction
|
||||
######################################################################
|
||||
ARG PY_VER=3.11.11-slim-bookworm
|
||||
ARG PY_VER=3.11.12-slim-bookworm
|
||||
|
||||
# If BUILDPLATFORM is null, set it to 'amd64' (or leave as is otherwise).
|
||||
ARG BUILDPLATFORM=${BUILDPLATFORM:-amd64}
|
||||
|
||||
@@ -103,7 +103,7 @@ Here are some of the major database solutions that are supported:
|
||||
|
||||
<p align="center">
|
||||
<img src="https://superset.apache.org/img/databases/redshift.png" alt="redshift" border="0" width="200"/>
|
||||
<img src="https://superset.apache.org/img/databases/google-biquery.png" alt="google-biquery" border="0" width="200"/>
|
||||
<img src="https://superset.apache.org/img/databases/google-biquery.png" alt="google-bigquery" border="0" width="200"/>
|
||||
<img src="https://superset.apache.org/img/databases/snowflake.png" alt="snowflake" border="0" width="200"/>
|
||||
<img src="https://superset.apache.org/img/databases/trino.png" alt="trino" border="0" width="150" />
|
||||
<img src="https://superset.apache.org/img/databases/presto.png" alt="presto" border="0" width="200"/>
|
||||
@@ -136,7 +136,7 @@ Here are some of the major database solutions that are supported:
|
||||
<img src="https://superset.apache.org/img/databases/starrocks.png" alt="starrocks" border="0" width="200" />
|
||||
<img src="https://superset.apache.org/img/databases/doris.png" alt="doris" border="0" width="200" />
|
||||
<img src="https://superset.apache.org/img/databases/oceanbase.svg" alt="oceanbase" border="0" width="220" />
|
||||
<img src="https://superset.apache.org/img/databases/sap-hana.png" alt="oceanbase" border="0" width="220" />
|
||||
<img src="https://superset.apache.org/img/databases/sap-hana.png" alt="sap-hana" border="0" width="220" />
|
||||
<img src="https://superset.apache.org/img/databases/denodo.png" alt="denodo" border="0" width="200" />
|
||||
<img src="https://superset.apache.org/img/databases/ydb.svg" alt="ydb" border="0" width="200" />
|
||||
<img src="https://superset.apache.org/img/databases/tdengine.png" alt="TDengine" border="0" width="200" />
|
||||
|
||||
@@ -43,6 +43,7 @@ Join our growing community!
|
||||
- [Cape Crypto](https://capecrypto.com)
|
||||
- [Capital Service S.A.](https://capitalservice.pl) [@pkonarzewski]
|
||||
- [Clark.de](https://clark.de/)
|
||||
- [Europace](https://europace.de)
|
||||
- [KarrotPay](https://www.daangnpay.com/)
|
||||
- [Remita](https://remita.net) [@mujibishola]
|
||||
- [Taveo](https://www.taveo.com) [@codek]
|
||||
@@ -104,6 +105,7 @@ Join our growing community!
|
||||
- [Formbricks](https://formbricks.com)
|
||||
- [Gavagai](https://gavagai.io) [@gavagai-corp]
|
||||
- [GfK Data Lab](https://www.gfk.com/home) [@mherr]
|
||||
- [HPE](https://www.hpe.com/in/en/home.html) [@anmol-hpe]
|
||||
- [Hydrolix](https://www.hydrolix.io/)
|
||||
- [Intercom](https://www.intercom.com/) [@kate-gallo]
|
||||
- [jampp](https://jampp.com/)
|
||||
|
||||
@@ -65,8 +65,6 @@ services:
|
||||
superset-init:
|
||||
condition: service_completed_successfully
|
||||
volumes: *superset-volumes
|
||||
environment:
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
|
||||
superset-init:
|
||||
image: *superset-image
|
||||
@@ -86,9 +84,6 @@ services:
|
||||
volumes: *superset-volumes
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
SUPERSET_LOAD_EXAMPLES: "${SUPERSET_LOAD_EXAMPLES:-yes}"
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
|
||||
superset-worker:
|
||||
image: *superset-image
|
||||
@@ -111,8 +106,6 @@ services:
|
||||
"CMD-SHELL",
|
||||
"celery -A superset.tasks.celery_app:app inspect ping -d celery@$$HOSTNAME",
|
||||
]
|
||||
environment:
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
|
||||
superset-worker-beat:
|
||||
image: *superset-image
|
||||
@@ -131,8 +124,6 @@ services:
|
||||
volumes: *superset-volumes
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
|
||||
volumes:
|
||||
superset_home:
|
||||
|
||||
@@ -71,8 +71,6 @@ services:
|
||||
superset-init:
|
||||
condition: service_completed_successfully
|
||||
volumes: *superset-volumes
|
||||
environment:
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
|
||||
superset-init:
|
||||
container_name: superset_init
|
||||
@@ -93,9 +91,6 @@ services:
|
||||
volumes: *superset-volumes
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
SUPERSET_LOAD_EXAMPLES: "${SUPERSET_LOAD_EXAMPLES:-yes}"
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
|
||||
superset-worker:
|
||||
build:
|
||||
@@ -119,8 +114,6 @@ services:
|
||||
"CMD-SHELL",
|
||||
"celery -A superset.tasks.celery_app:app inspect ping -d celery@$$HOSTNAME",
|
||||
]
|
||||
environment:
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
|
||||
superset-worker-beat:
|
||||
build:
|
||||
@@ -140,8 +133,6 @@ services:
|
||||
volumes: *superset-volumes
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
|
||||
volumes:
|
||||
superset_home:
|
||||
|
||||
@@ -104,9 +104,6 @@ services:
|
||||
superset-init:
|
||||
condition: service_completed_successfully
|
||||
volumes: *superset-volumes
|
||||
environment:
|
||||
CYPRESS_CONFIG: "${CYPRESS_CONFIG:-}"
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
|
||||
superset-websocket:
|
||||
container_name: superset_websocket
|
||||
@@ -158,10 +155,6 @@ services:
|
||||
condition: service_started
|
||||
user: *superset-user
|
||||
volumes: *superset-volumes
|
||||
environment:
|
||||
CYPRESS_CONFIG: "${CYPRESS_CONFIG:-}"
|
||||
SUPERSET_LOAD_EXAMPLES: "${SUPERSET_LOAD_EXAMPLES:-yes}"
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
healthcheck:
|
||||
disable: true
|
||||
|
||||
@@ -206,8 +199,6 @@ services:
|
||||
required: false
|
||||
environment:
|
||||
CELERYD_CONCURRENCY: 2
|
||||
CYPRESS_CONFIG: "${CYPRESS_CONFIG:-}"
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
superset-init:
|
||||
@@ -239,9 +230,6 @@ services:
|
||||
volumes: *superset-volumes
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
CYPRESS_CONFIG: "${CYPRESS_CONFIG:-}"
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
|
||||
superset-tests-worker:
|
||||
build:
|
||||
@@ -262,7 +250,6 @@ services:
|
||||
REDIS_RESULTS_DB: 3
|
||||
REDIS_HOST: localhost
|
||||
CELERYD_CONCURRENCY: 8
|
||||
SUPERSET_LOG_LEVEL: "${SUPERSET_LOG_LEVEL:-info}"
|
||||
network_mode: host
|
||||
depends_on:
|
||||
superset-init:
|
||||
|
||||
@@ -302,6 +302,15 @@ AUTH_USER_REGISTRATION = True
|
||||
AUTH_USER_REGISTRATION_ROLE = "Public"
|
||||
```
|
||||
|
||||
In case you want to assign the `Admin` role on new user registration, it can be assigned as follows:
|
||||
```python
|
||||
AUTH_USER_REGISTRATION_ROLE = "Admin"
|
||||
```
|
||||
If you encounter the [issue](https://github.com/apache/superset/issues/13243) of not being able to list users from the Superset main page settings, although a newly registered user has an `Admin` role, please re-run `superset init` to sync the required permissions. Below is the command to re-run `superset init` using docker compose.
|
||||
```
|
||||
docker-compose exec superset superset init
|
||||
```
|
||||
|
||||
Then, create a `CustomSsoSecurityManager` that extends `SupersetSecurityManager` and overrides
|
||||
`oauth_user_info`:
|
||||
|
||||
|
||||
@@ -72,7 +72,8 @@ are compatible with Superset.
|
||||
| [PostgreSQL](/docs/configuration/databases#postgres) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||
| [Presto](/docs/configuration/databases#presto) | `pip install pyhive` | `presto://{username}:{password}@{hostname}:{port}/{database}` |
|
||||
| [Rockset](/docs/configuration/databases#rockset) | `pip install rockset-sqlalchemy` | `rockset://<api_key>:@<api_server>` |
|
||||
| [SAP Hana](/docs/configuration/databases#hana) | `pip install hdbcli sqlalchemy-hana` or `pip install apache_superset[hana]` | `hana://{username}:{password}@{host}:{port}` |
|
||||
| [SAP Hana](/docs/configuration/databases#hana) | `pip install hdbcli sqlalchemy-hana` or `pip install apache_superset[hana]` | `hana://{username}:{password}@{host}:{port}` |
|
||||
| [SingleStore](/docs/configuration/databases#singlestore) | `pip install sqlalchemy-singlestoredb` | `singlestoredb://{username}:{password}@{host}:{port}/{database}` |
|
||||
| [StarRocks](/docs/configuration/databases#starrocks) | `pip install starrocks` | `starrocks://<User>:<Password>@<Host>:<Port>/<Catalog>.<Database>` |
|
||||
| [Snowflake](/docs/configuration/databases#snowflake) | `pip install snowflake-sqlalchemy` | `snowflake://{user}:{password}@{account}.{region}/{database}?role={role}&warehouse={warehouse}` |
|
||||
| SQLite | No additional library needed | `sqlite://path/to/file.db?check_same_thread=false` |
|
||||
@@ -1299,6 +1300,16 @@ You might have noticed that some special charecters are used in the above connec
|
||||
For more information about this check the [sqlalchemy documentation](https://docs.sqlalchemy.org/en/20/core/engines.html#escaping-special-characters-such-as-signs-in-passwords). Which says `When constructing a fully formed URL string to pass to create_engine(), special characters such as those that may be used in the user and password need to be URL encoded to be parsed correctly. This includes the @ sign.`
|
||||
:::
|
||||
|
||||
#### SingleStore
|
||||
|
||||
The recommended connector library for SingleStore is
|
||||
[sqlalchemy-singlestoredb](https://github.com/singlestore-labs/sqlalchemy-singlestoredb).
|
||||
|
||||
The expected connection string is formatted as follows:
|
||||
|
||||
```
|
||||
singlestoredb://{username}:{password}@{host}:{port}/{database}
|
||||
```
|
||||
|
||||
#### StarRocks
|
||||
|
||||
|
||||
@@ -250,6 +250,14 @@ Will be rendered as:
|
||||
SELECT * FROM users WHERE role IN ('admin', 'viewer')
|
||||
```
|
||||
|
||||
**Current User RLS Rules**
|
||||
|
||||
The `{{ current_user_rls_rules() }}` macro returns an array of RLS rules applied to the current dataset for the logged in user.
|
||||
|
||||
If you have caching enabled in your Superset configuration, then the list of RLS Rules will be used
|
||||
by Superset when calculating the cache key. A cache key is a unique identifier that determines if there's a
|
||||
cache hit in the future and Superset can retrieve cached data.
|
||||
|
||||
**Custom URL Parameters**
|
||||
|
||||
The `{{ url_param('custom_variable') }}` macro lets you define arbitrary URL
|
||||
|
||||
@@ -64,6 +64,56 @@ 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)
|
||||
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 \
|
||||
# openpyxl to be able to upload Excel files
|
||||
openpyxl \
|
||||
# Pillow for Alerts & Reports to generate PDFs of dashboards
|
||||
Pillow \
|
||||
# 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 install-deps \
|
||||
&& 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
|
||||
|
||||
- `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.
|
||||
|
||||
Ideally you will build your own image of Superset that extends `lean`, adding what your deployment needs.
|
||||
|
||||
See [Docker Build Presets](/docs/installation/docker-builds/#build-presets) for more information about the different image versions you can extend.
|
||||
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).
|
||||
|
||||
## [Kubernetes (K8s)](/docs/installation/kubernetes.mdx)
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
title: CVEs fixed by release
|
||||
sidebar_position: 2
|
||||
---
|
||||
#### Version 4.1.2
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|:---------------|:-----------------------------------------------------------------------------------|---------:|
|
||||
| CVE-2025-27696 | Improper authorization leading to resource ownership takeover | < 4.1.2 |
|
||||
| CVE-2025-48912 | Improper authorization bypass on row level security via SQL Injection | < 4.1.2 |
|
||||
|
||||
#### Version 4.1.0
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
"@emotion/styled": "^10.0.27",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@superset-ui/style": "^0.14.23",
|
||||
"antd": "^5.24.5",
|
||||
"antd": "^5.25.1",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"less": "^4.3.0",
|
||||
"less-loader": "^11.0.0",
|
||||
"less-loader": "^12.3.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -39,17 +39,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.7.0",
|
||||
"@docusaurus/tsconfig": "^3.7.0",
|
||||
"@docusaurus/tsconfig": "^3.8.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"prettier": "^2.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"webpack": "^5.99.7"
|
||||
"webpack": "^5.99.9"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
164
docs/yarn.lock
164
docs/yarn.lock
@@ -1092,20 +1092,13 @@
|
||||
core-js-pure "^3.30.2"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762"
|
||||
integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2":
|
||||
version "7.26.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2"
|
||||
integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/template@^7.25.9", "@babel/template@^7.26.9", "@babel/template@^7.27.0":
|
||||
version "7.27.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4"
|
||||
@@ -1942,10 +1935,10 @@
|
||||
fs-extra "^11.1.1"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@docusaurus/tsconfig@^3.7.0":
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz#654dcc524e25b8809af0f1b0b42485c18c047ab5"
|
||||
integrity sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ==
|
||||
"@docusaurus/tsconfig@^3.8.0":
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.8.0.tgz#ea7ee0917e1562cf0a6e95e049c42f1f61351f32"
|
||||
integrity sha512-utLl48nNjSYBoq47RKukZ9fPLEX3nJWThzrujb0ndQQ1jc/gh4RhTRaAqItH9nImnsgGKmLMnyoMBpfGmoop+w==
|
||||
|
||||
"@docusaurus/types@3.7.0":
|
||||
version "3.7.0"
|
||||
@@ -4186,10 +4179,10 @@ ansi-styles@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
antd@^5.24.5:
|
||||
version "5.24.5"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.24.5.tgz#b0976a113163888d1477f9e666c3c23352b098e9"
|
||||
integrity sha512-1lAv/G+9ewQanyoAo3JumQmIlVxwo5QwWGb6QCHYc40Cq0NxC/EzITcjsgq1PSaTUpLkKq8A2l7Fjtu47vqQBg==
|
||||
antd@^5.25.1:
|
||||
version "5.25.1"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.25.1.tgz#859b419a18d113492304ccd66c29074a71902241"
|
||||
integrity sha512-4KC7KuPCjr0z3Vuw9DsF+ceqJaPLbuUI3lOX1sY8ix25ceamp+P8yxOmk3Y2JHCD2ZAhq+5IQ/DTJRN2adWYKQ==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^7.2.0"
|
||||
"@ant-design/cssinjs" "^1.23.0"
|
||||
@@ -4206,37 +4199,37 @@ antd@^5.24.5:
|
||||
classnames "^2.5.1"
|
||||
copy-to-clipboard "^3.3.3"
|
||||
dayjs "^1.11.11"
|
||||
rc-cascader "~3.33.1"
|
||||
rc-cascader "~3.34.0"
|
||||
rc-checkbox "~3.5.0"
|
||||
rc-collapse "~3.9.0"
|
||||
rc-dialog "~9.6.0"
|
||||
rc-drawer "~7.2.0"
|
||||
rc-dropdown "~4.2.1"
|
||||
rc-field-form "~2.7.0"
|
||||
rc-image "~7.11.1"
|
||||
rc-input "~1.7.3"
|
||||
rc-input-number "~9.4.0"
|
||||
rc-mentions "~2.19.1"
|
||||
rc-image "~7.12.0"
|
||||
rc-input "~1.8.0"
|
||||
rc-input-number "~9.5.0"
|
||||
rc-mentions "~2.20.0"
|
||||
rc-menu "~9.16.1"
|
||||
rc-motion "^2.9.5"
|
||||
rc-notification "~5.6.3"
|
||||
rc-notification "~5.6.4"
|
||||
rc-pagination "~5.1.0"
|
||||
rc-picker "~4.11.3"
|
||||
rc-progress "~4.0.0"
|
||||
rc-rate "~2.13.1"
|
||||
rc-resize-observer "^1.4.3"
|
||||
rc-segmented "~2.7.0"
|
||||
rc-select "~14.16.6"
|
||||
rc-select "~14.16.7"
|
||||
rc-slider "~11.1.8"
|
||||
rc-steps "~6.0.1"
|
||||
rc-switch "~4.1.0"
|
||||
rc-table "~7.50.4"
|
||||
rc-tabs "~15.5.1"
|
||||
rc-textarea "~1.9.0"
|
||||
rc-tabs "~15.6.1"
|
||||
rc-textarea "~1.10.0"
|
||||
rc-tooltip "~6.4.0"
|
||||
rc-tree "~5.13.1"
|
||||
rc-tree-select "~5.27.0"
|
||||
rc-upload "~4.8.1"
|
||||
rc-upload "~4.9.0"
|
||||
rc-util "^5.44.4"
|
||||
scroll-into-view-if-needed "^3.1.0"
|
||||
throttle-debounce "^5.0.2"
|
||||
@@ -5674,12 +5667,7 @@ data-view-byte-offset@^1.0.1:
|
||||
es-errors "^1.3.0"
|
||||
is-data-view "^1.0.1"
|
||||
|
||||
dayjs@^1.11.11:
|
||||
version "1.11.12"
|
||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz"
|
||||
integrity sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==
|
||||
|
||||
dayjs@^1.11.13:
|
||||
dayjs@^1.11.11, dayjs@^1.11.13:
|
||||
version "1.11.13"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
|
||||
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"
|
||||
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
|
||||
|
||||
eslint-config-prettier@^10.1.2:
|
||||
version "10.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz#31a4b393c40c4180202c27e829af43323bf85276"
|
||||
integrity sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==
|
||||
eslint-config-prettier@^10.1.5:
|
||||
version "10.1.5"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
|
||||
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
|
||||
|
||||
eslint-plugin-prettier@^4.0.0:
|
||||
version "4.2.1"
|
||||
@@ -8223,10 +8211,10 @@ layout-base@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285"
|
||||
integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==
|
||||
|
||||
less-loader@^11.0.0:
|
||||
version "11.1.4"
|
||||
resolved "https://registry.npmjs.org/less-loader/-/less-loader-11.1.4.tgz"
|
||||
integrity sha512-6/GrYaB6QcW6Vj+/9ZPgKKs6G10YZai/l/eJ4SLwbzqNTBsAqt5hSLVF47TgsiBxV1P6eAU0GYRH3YRuQU9V3A==
|
||||
less-loader@^12.3.0:
|
||||
version "12.3.0"
|
||||
resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-12.3.0.tgz#d4a00361568be86a97da3df4f16954b0d4c15340"
|
||||
integrity sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==
|
||||
|
||||
less@^4.3.0:
|
||||
version "4.3.0"
|
||||
@@ -10617,10 +10605,10 @@ raw-body@2.5.2:
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
rc-cascader@~3.33.1:
|
||||
version "3.33.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.33.1.tgz#19e01462ef5ef51b723c1f562c7b9cde4691e7ee"
|
||||
integrity sha512-Kyl4EJ7ZfCBuidmZVieegcbFw0RcU5bHHSbtEdmuLYd0fYHCAiYKZ6zon7fWAVyC6rWWOOib0XKdTSf7ElC9rg==
|
||||
rc-cascader@~3.34.0:
|
||||
version "3.34.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.34.0.tgz#56f936ab6b1229bab7d558701ce9b9e96536582c"
|
||||
integrity sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.25.7"
|
||||
classnames "^2.3.1"
|
||||
@@ -10688,10 +10676,10 @@ rc-field-form@~2.7.0:
|
||||
"@rc-component/async-validator" "^5.0.3"
|
||||
rc-util "^5.32.2"
|
||||
|
||||
rc-image@~7.11.1:
|
||||
version "7.11.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-7.11.1.tgz#3ab290708dc053d3681de94186522e4e594f6772"
|
||||
integrity sha512-XuoWx4KUXg7hNy5mRTy1i8c8p3K8boWg6UajbHpDXS5AlRVucNfTi5YxTtPBTBzegxAZpvuLfh3emXFt6ybUdA==
|
||||
rc-image@~7.12.0:
|
||||
version "7.12.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-7.12.0.tgz#95e9314701e668217d113c1f29b4f01ac025cafe"
|
||||
integrity sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
"@rc-component/portal" "^1.0.2"
|
||||
@@ -10700,37 +10688,37 @@ rc-image@~7.11.1:
|
||||
rc-motion "^2.6.2"
|
||||
rc-util "^5.34.1"
|
||||
|
||||
rc-input-number@~9.4.0:
|
||||
version "9.4.0"
|
||||
resolved "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.4.0.tgz"
|
||||
integrity sha512-Tiy4DcXcFXAf9wDhN8aUAyMeCLHJUHA/VA/t7Hj8ZEx5ETvxG7MArDOSE6psbiSCo+vJPm4E3fGN710ITVn6GA==
|
||||
rc-input-number@~9.5.0:
|
||||
version "9.5.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-9.5.0.tgz#b47963d0f2cbd85ab2f1badfdc089a904c073f38"
|
||||
integrity sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
"@rc-component/mini-decimal" "^1.0.1"
|
||||
classnames "^2.2.5"
|
||||
rc-input "~1.7.1"
|
||||
rc-input "~1.8.0"
|
||||
rc-util "^5.40.1"
|
||||
|
||||
rc-input@~1.7.1, rc-input@~1.7.3:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-1.7.3.tgz#cb334a17b93ce985bceb243b4c111a5ed641e0e3"
|
||||
integrity sha512-A5w4egJq8+4JzlQ55FfQjDnPvOaAbzwC3VLOAdOytyek3TboSOP9qxN+Gifup+shVXfvecBLBbWBpWxmk02SWQ==
|
||||
rc-input@~1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-1.8.0.tgz#d2f4404befebf2fbdc28390d5494c302f74ae974"
|
||||
integrity sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.1"
|
||||
classnames "^2.2.1"
|
||||
rc-util "^5.18.1"
|
||||
|
||||
rc-mentions@~2.19.1:
|
||||
version "2.19.1"
|
||||
resolved "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.19.1.tgz"
|
||||
integrity sha512-KK3bAc/bPFI993J3necmaMXD2reZTzytZdlTvkeBbp50IGH1BDPDvxLdHDUrpQx2b2TGaVJsn+86BvYa03kGqA==
|
||||
rc-mentions@~2.20.0:
|
||||
version "2.20.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-mentions/-/rc-mentions-2.20.0.tgz#3bbeac0352b02e0ce3e1244adb48701bb6903bf7"
|
||||
integrity sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.22.5"
|
||||
"@rc-component/trigger" "^2.0.0"
|
||||
classnames "^2.2.6"
|
||||
rc-input "~1.7.1"
|
||||
rc-input "~1.8.0"
|
||||
rc-menu "~9.16.0"
|
||||
rc-textarea "~1.9.0"
|
||||
rc-textarea "~1.10.0"
|
||||
rc-util "^5.34.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"
|
||||
rc-util "^5.44.0"
|
||||
|
||||
rc-notification@~5.6.3:
|
||||
version "5.6.3"
|
||||
resolved "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.3.tgz"
|
||||
integrity sha512-42szwnn8VYQoT6GnjO00i1iwqV9D1TTMvxObWsuLwgl0TsOokzhkYiufdtQBsJMFjJravS1hfDKVMHLKLcPE4g==
|
||||
rc-notification@~5.6.4:
|
||||
version "5.6.4"
|
||||
resolved "https://registry.yarnpkg.com/rc-notification/-/rc-notification-5.6.4.tgz#ea89c39c13cd517fdfd97fe63f03376fabb78544"
|
||||
integrity sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "2.x"
|
||||
@@ -10833,10 +10821,10 @@ rc-segmented@~2.7.0:
|
||||
rc-motion "^2.4.4"
|
||||
rc-util "^5.17.0"
|
||||
|
||||
rc-select@~14.16.2, rc-select@~14.16.6:
|
||||
version "14.16.6"
|
||||
resolved "https://registry.npmjs.org/rc-select/-/rc-select-14.16.6.tgz"
|
||||
integrity sha512-YPMtRPqfZWOm2XGTbx5/YVr1HT0vn//8QS77At0Gjb3Lv+Lbut0IORJPKLWu1hQ3u4GsA0SrDzs7nI8JG7Zmyg==
|
||||
rc-select@~14.16.2, rc-select@~14.16.7:
|
||||
version "14.16.8"
|
||||
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.16.8.tgz#78e6782f1ccc1f03d9003bc3effa4ed609d29a97"
|
||||
integrity sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
"@rc-component/trigger" "^2.1.1"
|
||||
@@ -10885,10 +10873,10 @@ rc-table@~7.50.4:
|
||||
rc-util "^5.44.3"
|
||||
rc-virtual-list "^3.14.2"
|
||||
|
||||
rc-tabs@~15.5.1:
|
||||
version "15.5.1"
|
||||
resolved "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.1.tgz"
|
||||
integrity sha512-yiWivLAjEo5d1v2xlseB2dQocsOhkoVSfo1krS8v8r+02K+TBUjSjXIf7dgyVSxp6wRIPv5pMi5hanNUlQMgUA==
|
||||
rc-tabs@~15.6.1:
|
||||
version "15.6.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-15.6.1.tgz#f0b6c65384dfa09a64eb539e86a0667c7a650708"
|
||||
integrity sha512-/HzDV1VqOsUWyuC0c6AkxVYFjvx9+rFPKZ32ejxX0Uc7QCzcEjTA9/xMgv4HemPKwzBNX8KhGVbbumDjnj92aA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
classnames "2.x"
|
||||
@@ -10898,14 +10886,14 @@ rc-tabs@~15.5.1:
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.34.1"
|
||||
|
||||
rc-textarea@~1.9.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.9.0.tgz"
|
||||
integrity sha512-dQW/Bc/MriPBTugj2Kx9PMS5eXCCGn2cxoIaichjbNvOiARlaHdI99j4DTxLl/V8+PIfW06uFy7kjfUIDDKyxQ==
|
||||
rc-textarea@~1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.0.tgz#f8f962ef83be0b8e35db97cf03dbfb86ddd9c46c"
|
||||
integrity sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.1"
|
||||
rc-input "~1.7.1"
|
||||
rc-input "~1.8.0"
|
||||
rc-resize-observer "^1.0.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-virtual-list "^3.5.1"
|
||||
|
||||
rc-upload@~4.8.1:
|
||||
version "4.8.1"
|
||||
resolved "https://registry.npmjs.org/rc-upload/-/rc-upload-4.8.1.tgz"
|
||||
integrity sha512-toEAhwl4hjLAI1u8/CgKWt30BR06ulPa4iGQSMvSXoHzO88gPCslxqV/mnn4gJU7PDoltGIC9Eh+wkeudqgHyw==
|
||||
rc-upload@~4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.9.0.tgz#911963ab5a0b538c743765371c05e2de9e3f5436"
|
||||
integrity sha512-pAzlPnyiFn1GCtEybEG2m9nXNzQyWXqWV2xFYCmDxjN9HzyjS5Pz2F+pbNdYw8mMJsixLEKLG0wVy9vOGxJMJA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.3"
|
||||
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"
|
||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||
|
||||
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.7:
|
||||
version "5.99.7"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.7.tgz#60201c1ca66da046b07d006c2f6e0cc5e8a7bdba"
|
||||
integrity sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==
|
||||
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9:
|
||||
version "5.99.9"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247"
|
||||
integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.6"
|
||||
|
||||
@@ -32,6 +32,7 @@ authors = [
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
dependencies = [
|
||||
"backoff>=1.8.0",
|
||||
@@ -44,7 +45,7 @@ dependencies = [
|
||||
"cryptography>=42.0.4, <45.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <3.0.0",
|
||||
"flask-appbuilder>=4.6.3, <5.0.0",
|
||||
"flask-appbuilder>=4.7.0, <5.0.0",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
@@ -66,7 +67,7 @@ dependencies = [
|
||||
"markdown>=3.0",
|
||||
"msgpack>=1.0.0, <1.1",
|
||||
"nh3>=0.2.11, <0.3",
|
||||
"numpy>1.23.5, <2",
|
||||
"numpy>1.23.5, <2.3",
|
||||
"packaging",
|
||||
# --------------------------
|
||||
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
|
||||
@@ -93,8 +94,8 @@ dependencies = [
|
||||
"sqlalchemy>=1.4, <2",
|
||||
"sqlalchemy-utils>=0.38.3, <0.39",
|
||||
"sqlglot>=26.1.3, <27",
|
||||
"sqlparse>=0.5.0",
|
||||
"tabulate>=0.8.9, <0.9",
|
||||
# newer pandas needs 0.9+
|
||||
"tabulate>=0.9.0, <1.0",
|
||||
"typing-extensions>=4, <5",
|
||||
"waitress; sys_platform == 'win32'",
|
||||
"wtforms>=2.3.3, <4",
|
||||
@@ -166,6 +167,7 @@ prophet = ["prophet>=1.1.5, <2"]
|
||||
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
|
||||
rockset = ["rockset-sqlalchemy>=0.0.1, <1"]
|
||||
shillelagh = ["shillelagh[all]>=1.2.18, <2"]
|
||||
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]
|
||||
snowflake = ["snowflake-sqlalchemy>=1.2.4, <2"]
|
||||
spark = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
@@ -216,7 +218,7 @@ combine_as_imports = true
|
||||
include_trailing_comma = true
|
||||
line_length = 88
|
||||
known_first_party = "superset"
|
||||
known_third_party = "alembic, apispec, backoff, celery, click, colorama, cron_descriptor, croniter, cryptography, dateutil, deprecation, flask, flask_appbuilder, flask_babel, flask_caching, flask_compress, flask_jwt_extended, flask_login, flask_migrate, flask_sqlalchemy, flask_talisman, flask_testing, flask_wtf, freezegun, geohash, geopy, holidays, humanize, isodate, jinja2, jwt, markdown, markupsafe, marshmallow, msgpack, nh3, numpy, pandas, parameterized, parsedatetime, pgsanity, polyline, prison, progress, pyarrow, sqlalchemy_bigquery, pyhive, pyparsing, pytest, pytest_mock, pytz, redis, requests, selenium, setuptools, shillelagh, simplejson, slack, sqlalchemy, sqlalchemy_utils, sqlparse, typing_extensions, urllib3, werkzeug, wtforms, wtforms_json, yaml"
|
||||
known_third_party = "alembic, apispec, backoff, celery, click, colorama, cron_descriptor, croniter, cryptography, dateutil, deprecation, flask, flask_appbuilder, flask_babel, flask_caching, flask_compress, flask_jwt_extended, flask_login, flask_migrate, flask_sqlalchemy, flask_talisman, flask_testing, flask_wtf, freezegun, geohash, geopy, holidays, humanize, isodate, jinja2, jwt, markdown, markupsafe, marshmallow, msgpack, nh3, numpy, pandas, parameterized, parsedatetime, pgsanity, polyline, prison, progress, pyarrow, sqlalchemy_bigquery, pyhive, pyparsing, pytest, pytest_mock, pytz, redis, requests, selenium, setuptools, shillelagh, simplejson, slack, sqlalchemy, sqlalchemy_utils, typing_extensions, urllib3, werkzeug, wtforms, wtforms_json, yaml"
|
||||
multi_line_output = 3
|
||||
order_by_type = false
|
||||
|
||||
@@ -240,6 +242,12 @@ disallow_untyped_calls = false
|
||||
disallow_untyped_defs = false
|
||||
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]
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
@@ -272,7 +280,6 @@ exclude = [
|
||||
"venv",
|
||||
]
|
||||
|
||||
|
||||
# Same as Black.
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
@@ -367,6 +374,7 @@ docstring-code-line-length = "dynamic"
|
||||
requirement_txt_file = "requirements/base.txt"
|
||||
authorized_licenses = [
|
||||
"academic free license (afl)",
|
||||
"any-osi",
|
||||
"apache license 2.0",
|
||||
"apache software",
|
||||
"apache software, bsd",
|
||||
@@ -380,6 +388,7 @@ authorized_licenses = [
|
||||
"osi approved",
|
||||
"psf-2.0",
|
||||
"python software foundation",
|
||||
"simplified bsd",
|
||||
"the unlicense (unlicense)",
|
||||
"the unlicense",
|
||||
]
|
||||
|
||||
13
requirements/README.md
Normal file
13
requirements/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## Python dependency logic
|
||||
|
||||
In this folder, the `.in` files, in conjunction with the `../pyproject.toml` file (in the root of the repo) are used to generate the pinned requirements as `.txt` files.
|
||||
|
||||
To alter the pinned dependency, you can edit/alter the `.in` and `pyproject.toml` files, and then run the following command:
|
||||
|
||||
```bash
|
||||
./scripts/uv-pip-compile.sh
|
||||
```
|
||||
|
||||
This will generate the pinned requirements in the `.txt` files, which will be used in our CI/CD pipelines and in the Docker images.
|
||||
|
||||
We recommend to everyone in the community to use the pinned requirements in their local development environments, to ensure consistency across different environments, though we don't force requirements as part of our python package semantics to allow flexibility for users to install different versions of the dependencies if they wish.
|
||||
@@ -33,3 +33,6 @@ apispec>=6.0.0,<6.7.0
|
||||
# https://marshmallow-sqlalchemy.readthedocs.io/en/latest/changelog.html#id3
|
||||
# Opened this issue https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/665
|
||||
marshmallow-sqlalchemy>=1.3.0,<1.4.1
|
||||
|
||||
# needed for python 3.12 support
|
||||
openapi-schema-validator>=0.6.3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml requirements/base.in -o requirements/base.txt
|
||||
alembic==1.15.1
|
||||
alembic==1.15.2
|
||||
# via flask-migrate
|
||||
amqp==5.3.1
|
||||
# via kombu
|
||||
@@ -8,12 +8,10 @@ apispec==6.6.1
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# flask-appbuilder
|
||||
apsw==3.49.1.0
|
||||
apsw==3.49.2.0
|
||||
# via shillelagh
|
||||
async-timeout==4.0.3
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# redis
|
||||
# via -r requirements/base.in
|
||||
attrs==25.3.0
|
||||
# via
|
||||
# cattrs
|
||||
@@ -32,7 +30,7 @@ billiard==4.2.1
|
||||
# via celery
|
||||
blinker==1.9.0
|
||||
# via flask
|
||||
bottleneck==1.4.2
|
||||
bottleneck==1.5.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
brotli==1.1.0
|
||||
# via flask-compress
|
||||
@@ -42,11 +40,11 @@ cachelib==0.13.0
|
||||
# flask-session
|
||||
cachetools==5.5.2
|
||||
# via google-auth
|
||||
cattrs==24.1.2
|
||||
cattrs==24.1.3
|
||||
# via requests-cache
|
||||
celery==5.5.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
certifi==2025.1.31
|
||||
certifi==2025.4.26
|
||||
# via
|
||||
# requests
|
||||
# selenium
|
||||
@@ -54,9 +52,9 @@ cffi==1.17.1
|
||||
# via
|
||||
# cryptography
|
||||
# pynacl
|
||||
charset-normalizer==3.4.1
|
||||
charset-normalizer==3.4.2
|
||||
# via requests
|
||||
click==8.1.8
|
||||
click==8.2.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# celery
|
||||
@@ -99,11 +97,6 @@ email-validator==2.2.0
|
||||
# via flask-appbuilder
|
||||
et-xmlfile==2.0.0
|
||||
# via openpyxl
|
||||
exceptiongroup==1.2.2
|
||||
# via
|
||||
# cattrs
|
||||
# trio
|
||||
# trio-websocket
|
||||
flask==2.3.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
@@ -118,7 +111,7 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.6.3
|
||||
flask-appbuilder==4.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-babel==2.0.0
|
||||
# via flask-appbuilder
|
||||
@@ -152,13 +145,12 @@ geographiclib==2.0
|
||||
# via geopy
|
||||
geopy==2.4.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
google-auth==2.38.0
|
||||
google-auth==2.40.1
|
||||
# via shillelagh
|
||||
greenlet==3.1.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
gunicorn==23.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
h11==0.16.0
|
||||
@@ -174,6 +166,7 @@ idna==3.10
|
||||
# email-validator
|
||||
# requests
|
||||
# trio
|
||||
# url-normalize
|
||||
importlib-metadata==8.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
isodate==0.7.2
|
||||
@@ -189,9 +182,13 @@ jinja2==3.1.6
|
||||
jsonpath-ng==1.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
jsonschema==4.23.0
|
||||
# via flask-appbuilder
|
||||
jsonschema-specifications==2024.10.1
|
||||
# via jsonschema
|
||||
# via
|
||||
# flask-appbuilder
|
||||
# openapi-schema-validator
|
||||
jsonschema-specifications==2025.4.1
|
||||
# via
|
||||
# jsonschema
|
||||
# openapi-schema-validator
|
||||
kombu==5.5.3
|
||||
# via celery
|
||||
korean-lunar-calendar==0.3.1
|
||||
@@ -238,12 +235,16 @@ numpy==1.26.4
|
||||
# pandas
|
||||
odfpy==1.4.1
|
||||
# via pandas
|
||||
openapi-schema-validator==0.6.3
|
||||
# via -r requirements/base.in
|
||||
openpyxl==3.1.5
|
||||
# via pandas
|
||||
ordered-set==4.1.0
|
||||
# via flask-limiter
|
||||
outcome==1.3.0.post0
|
||||
# via trio
|
||||
# via
|
||||
# trio
|
||||
# trio-websocket
|
||||
packaging==25.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
@@ -263,7 +264,7 @@ parsedatetime==2.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
pgsanity==0.2.9
|
||||
# via apache-superset (pyproject.toml)
|
||||
platformdirs==4.3.7
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
ply==3.11
|
||||
# via jsonpath-ng
|
||||
@@ -279,7 +280,7 @@ pyasn1==0.6.1
|
||||
# via
|
||||
# pyasn1-modules
|
||||
# rsa
|
||||
pyasn1-modules==0.4.1
|
||||
pyasn1-modules==0.4.2
|
||||
# via google-auth
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
@@ -334,15 +335,17 @@ requests==2.32.3
|
||||
# shillelagh
|
||||
requests-cache==1.2.1
|
||||
# via shillelagh
|
||||
rfc3339-validator==0.1.4
|
||||
# via openapi-schema-validator
|
||||
rich==13.9.4
|
||||
# via flask-limiter
|
||||
rpds-py==0.23.1
|
||||
rpds-py==0.25.0
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
rsa==4.9
|
||||
rsa==4.9.1
|
||||
# via google-auth
|
||||
selenium==4.27.1
|
||||
selenium==4.32.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
shillelagh==1.3.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -352,6 +355,7 @@ six==1.17.0
|
||||
# via
|
||||
# prison
|
||||
# python-dateutil
|
||||
# rfc3339-validator
|
||||
# url-normalize
|
||||
# wtforms-json
|
||||
slack-sdk==3.35.0
|
||||
@@ -373,36 +377,32 @@ sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
sqlglot==26.16.4
|
||||
# via apache-superset (pyproject.toml)
|
||||
sqlparse==0.5.3
|
||||
sqlglot==26.17.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
sshtunnel==0.4.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
tabulate==0.8.10
|
||||
tabulate==0.9.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
trio==0.28.0
|
||||
trio==0.30.0
|
||||
# via
|
||||
# selenium
|
||||
# trio-websocket
|
||||
trio-websocket==0.11.1
|
||||
trio-websocket==0.12.2
|
||||
# via selenium
|
||||
typing-extensions==4.12.2
|
||||
typing-extensions==4.13.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# alembic
|
||||
# cattrs
|
||||
# limits
|
||||
# pyopenssl
|
||||
# referencing
|
||||
# rich
|
||||
# selenium
|
||||
# shillelagh
|
||||
tzdata==2025.2
|
||||
# via
|
||||
# kombu
|
||||
# pandas
|
||||
url-normalize==1.4.3
|
||||
url-normalize==2.2.1
|
||||
# via requests-cache
|
||||
urllib3==1.26.20
|
||||
# via
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# uv pip compile requirements/development.in -c requirements/base.txt -o requirements/development.txt
|
||||
-e .
|
||||
# via -r requirements/development.in
|
||||
alembic==1.15.1
|
||||
alembic==1.15.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-migrate
|
||||
@@ -14,14 +14,10 @@ apispec==6.6.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
apsw==3.49.1.0
|
||||
apsw==3.49.2.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# shillelagh
|
||||
async-timeout==4.0.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# redis
|
||||
attrs==25.3.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -51,7 +47,7 @@ blinker==1.9.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask
|
||||
bottleneck==1.4.2
|
||||
bottleneck==1.5.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -68,7 +64,7 @@ cachetools==5.5.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-auth
|
||||
cattrs==24.1.2
|
||||
cattrs==24.1.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests-cache
|
||||
@@ -76,7 +72,7 @@ celery==5.5.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
certifi==2025.1.31
|
||||
certifi==2025.4.26
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests
|
||||
@@ -88,11 +84,11 @@ cffi==1.17.1
|
||||
# pynacl
|
||||
cfgv==3.4.0
|
||||
# via pre-commit
|
||||
charset-normalizer==3.4.1
|
||||
charset-normalizer==3.4.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests
|
||||
click==8.1.8
|
||||
click==8.2.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -176,13 +172,6 @@ et-xmlfile==2.0.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# openpyxl
|
||||
exceptiongroup==1.2.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# cattrs
|
||||
# pytest
|
||||
# trio
|
||||
# trio-websocket
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
flask==2.3.3
|
||||
@@ -202,7 +191,7 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.6.3
|
||||
flask-appbuilder==4.7.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -280,7 +269,7 @@ google-api-core==2.23.0
|
||||
# google-cloud-core
|
||||
# pandas-gbq
|
||||
# sqlalchemy-bigquery
|
||||
google-auth==2.38.0
|
||||
google-auth==2.40.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-api-core
|
||||
@@ -318,7 +307,6 @@ greenlet==3.1.1
|
||||
# apache-superset
|
||||
# gevent
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
grpcio==1.71.0
|
||||
# via
|
||||
# apache-superset
|
||||
@@ -355,6 +343,7 @@ idna==3.10
|
||||
# email-validator
|
||||
# requests
|
||||
# trio
|
||||
# url-normalize
|
||||
importlib-metadata==8.7.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -389,7 +378,7 @@ jsonschema==4.23.0
|
||||
# openapi-spec-validator
|
||||
jsonschema-path==0.3.4
|
||||
# via openapi-spec-validator
|
||||
jsonschema-specifications==2024.10.1
|
||||
jsonschema-specifications==2025.4.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# jsonschema
|
||||
@@ -480,7 +469,9 @@ odfpy==1.4.1
|
||||
# -c requirements/base.txt
|
||||
# pandas
|
||||
openapi-schema-validator==0.6.3
|
||||
# via openapi-spec-validator
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# openapi-spec-validator
|
||||
openapi-spec-validator==0.7.1
|
||||
# via apache-superset
|
||||
openpyxl==3.1.5
|
||||
@@ -495,6 +486,7 @@ outcome==1.3.0.post0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# trio
|
||||
# trio-websocket
|
||||
packaging==25.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -542,7 +534,7 @@ pillow==10.3.0
|
||||
# via
|
||||
# apache-superset
|
||||
# matplotlib
|
||||
platformdirs==4.3.7
|
||||
platformdirs==4.3.8
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests-cache
|
||||
@@ -598,7 +590,7 @@ pyasn1==0.6.1
|
||||
# pyasn1-modules
|
||||
# python-ldap
|
||||
# rsa
|
||||
pyasn1-modules==0.4.1
|
||||
pyasn1-modules==0.4.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-auth
|
||||
@@ -726,27 +718,29 @@ requests-cache==1.2.1
|
||||
requests-oauthlib==2.0.0
|
||||
# via google-auth-oauthlib
|
||||
rfc3339-validator==0.1.4
|
||||
# via openapi-schema-validator
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# openapi-schema-validator
|
||||
rich==13.9.4
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-limiter
|
||||
rpds-py==0.23.1
|
||||
rpds-py==0.25.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# jsonschema
|
||||
# referencing
|
||||
rsa==4.9
|
||||
rsa==4.9.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-auth
|
||||
ruff==0.8.0
|
||||
# via apache-superset
|
||||
selenium==4.27.1
|
||||
selenium==4.32.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
setuptools==75.6.0
|
||||
setuptools==80.7.1
|
||||
# via
|
||||
# nodeenv
|
||||
# pandas-gbq
|
||||
@@ -767,7 +761,6 @@ six==1.17.0
|
||||
# prison
|
||||
# python-dateutil
|
||||
# rfc3339-validator
|
||||
# url-normalize
|
||||
# wtforms-json
|
||||
slack-sdk==3.35.0
|
||||
# via
|
||||
@@ -799,55 +792,45 @@ sqlalchemy-utils==0.38.3
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
sqlglot==26.16.4
|
||||
sqlglot==26.17.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
sqloxide==0.1.51
|
||||
# via apache-superset
|
||||
sqlparse==0.5.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
sshtunnel==0.4.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
statsd==4.0.1
|
||||
# via apache-superset
|
||||
tabulate==0.8.10
|
||||
tabulate==0.9.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
tomli==2.2.1
|
||||
# via
|
||||
# coverage
|
||||
# pytest
|
||||
tqdm==4.67.1
|
||||
# via
|
||||
# cmdstanpy
|
||||
# prophet
|
||||
trino==0.330.0
|
||||
# via apache-superset
|
||||
trio==0.28.0
|
||||
trio==0.30.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# selenium
|
||||
# trio-websocket
|
||||
trio-websocket==0.11.1
|
||||
trio-websocket==0.12.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# selenium
|
||||
typing-extensions==4.12.2
|
||||
typing-extensions==4.13.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# alembic
|
||||
# apache-superset
|
||||
# cattrs
|
||||
# limits
|
||||
# pyopenssl
|
||||
# referencing
|
||||
# rich
|
||||
# selenium
|
||||
# shillelagh
|
||||
tzdata==2025.2
|
||||
@@ -857,7 +840,7 @@ tzdata==2025.2
|
||||
# pandas
|
||||
tzlocal==5.2
|
||||
# via trino
|
||||
url-normalize==1.4.3
|
||||
url-normalize==2.2.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests-cache
|
||||
|
||||
@@ -252,4 +252,215 @@ describe('Visualization > Table', () => {
|
||||
});
|
||||
cy.get('td').contains(/\d*%/);
|
||||
});
|
||||
|
||||
it('Test row limit with server pagination toggle', () => {
|
||||
cy.visitChartByParams({
|
||||
...VIZ_DEFAULTS,
|
||||
metrics: ['count'],
|
||||
row_limit: 100,
|
||||
});
|
||||
|
||||
// Enable server pagination
|
||||
cy.get('[data-test="server_pagination-header"] div.pull-left').click();
|
||||
|
||||
// Click row limit control and select high value (200k)
|
||||
cy.get('div[aria-label="Row limit"]').click();
|
||||
|
||||
// Type 200000 and press enter to select the option
|
||||
cy.get('div[aria-label="Row limit"]')
|
||||
.find('.ant-select-selection-search-input:visible')
|
||||
.type('200000{enter}');
|
||||
|
||||
// Verify that there is no error tooltip when server pagination is enabled
|
||||
cy.get('[data-test="error-tooltip"]').should('not.exist');
|
||||
|
||||
// Disable server pagination
|
||||
cy.get('[data-test="server_pagination-header"] div.pull-left').click();
|
||||
|
||||
// Verify error tooltip appears
|
||||
cy.get('[data-test="error-tooltip"]').should('be.visible');
|
||||
|
||||
// Trigger mouseover and verify tooltip text
|
||||
cy.get('[data-test="error-tooltip"]').trigger('mouseover');
|
||||
|
||||
// Verify tooltip content
|
||||
cy.get('.antd5-tooltip-inner').should('be.visible');
|
||||
cy.get('.antd5-tooltip-inner').should(
|
||||
'contain',
|
||||
'Server pagination needs to be enabled for values over',
|
||||
);
|
||||
|
||||
// Hide the tooltip by adding display:none style
|
||||
cy.get('.antd5-tooltip').invoke('attr', 'style', 'display: none');
|
||||
|
||||
// Enable server pagination again
|
||||
cy.get('[data-test="server_pagination-header"] div.pull-left').click();
|
||||
|
||||
cy.get('[data-test="error-tooltip"]').should('not.exist');
|
||||
|
||||
cy.get('div[aria-label="Row limit"]').click();
|
||||
|
||||
// Type 1000000
|
||||
cy.get('div[aria-label="Row limit"]')
|
||||
.find('.ant-select-selection-search-input:visible')
|
||||
.type('1000000');
|
||||
|
||||
// Wait for 1 second
|
||||
cy.wait(1000);
|
||||
|
||||
// Press enter
|
||||
cy.get('div[aria-label="Row limit"]')
|
||||
.find('.ant-select-selection-search-input:visible')
|
||||
.type('{enter}');
|
||||
|
||||
// Wait for error tooltip to appear and verify its content
|
||||
cy.get('[data-test="error-tooltip"]')
|
||||
.should('be.visible')
|
||||
.trigger('mouseover');
|
||||
|
||||
// Wait for tooltip content and verify
|
||||
cy.get('.antd5-tooltip-inner').should('exist');
|
||||
cy.get('.antd5-tooltip-inner').should('be.visible');
|
||||
|
||||
// Verify tooltip content separately
|
||||
cy.get('.antd5-tooltip-inner').should('contain', 'Value cannot exceed');
|
||||
});
|
||||
|
||||
it('Test sorting with server pagination enabled', () => {
|
||||
cy.visitChartByParams({
|
||||
...VIZ_DEFAULTS,
|
||||
metrics: ['count'],
|
||||
groupby: ['name'],
|
||||
row_limit: 100000,
|
||||
server_pagination: true, // Enable server pagination
|
||||
});
|
||||
|
||||
// Wait for the initial data load
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Get the first column header (name)
|
||||
cy.get('.chart-container th').contains('name').as('nameHeader');
|
||||
|
||||
// Click to sort ascending
|
||||
cy.get('@nameHeader').click();
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Verify first row starts with 'A'
|
||||
cy.get('.chart-container td:first').invoke('text').should('match', /^[Aa]/);
|
||||
|
||||
// Click again to sort descending
|
||||
cy.get('@nameHeader').click();
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Verify first row starts with 'Z'
|
||||
cy.get('.chart-container td:first').invoke('text').should('match', /^[Zz]/);
|
||||
|
||||
// Test numeric sorting
|
||||
cy.get('.chart-container th').contains('COUNT').as('countHeader');
|
||||
|
||||
// Click to sort ascending by count
|
||||
cy.get('@countHeader').click();
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Get first two count values and verify ascending order
|
||||
cy.get('.chart-container td:nth-child(2)').then($cells => {
|
||||
const first = parseFloat($cells[0].textContent || '0');
|
||||
const second = parseFloat($cells[1].textContent || '0');
|
||||
expect(first).to.be.at.most(second);
|
||||
});
|
||||
|
||||
// Click again to sort descending
|
||||
cy.get('@countHeader').click();
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Get first two count values and verify descending order
|
||||
cy.get('.chart-container td:nth-child(2)').then($cells => {
|
||||
const first = parseFloat($cells[0].textContent || '0');
|
||||
const second = parseFloat($cells[1].textContent || '0');
|
||||
expect(first).to.be.at.least(second);
|
||||
});
|
||||
});
|
||||
|
||||
it('Test search with server pagination enabled', () => {
|
||||
cy.visitChartByParams({
|
||||
...VIZ_DEFAULTS,
|
||||
metrics: ['count'],
|
||||
groupby: ['name', 'state'],
|
||||
row_limit: 100000,
|
||||
server_pagination: true,
|
||||
include_search: true,
|
||||
});
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
// Basic search test
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').should(
|
||||
'be.visible',
|
||||
);
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').type('John');
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
cy.get('.chart-container tbody tr').each($row => {
|
||||
cy.wrap($row).contains(/John/i);
|
||||
});
|
||||
|
||||
// Clear and test case-insensitive search
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').clear();
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').type('mary');
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
cy.get('.chart-container tbody tr').each($row => {
|
||||
cy.wrap($row).contains(/Mary/i);
|
||||
});
|
||||
|
||||
// Test special characters
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').clear();
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').type('Nicole');
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
cy.get('.chart-container tbody tr').each($row => {
|
||||
cy.wrap($row).contains(/Nicole/i);
|
||||
});
|
||||
|
||||
// Test no results
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').clear();
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').type('XYZ123');
|
||||
|
||||
cy.wait('@chartData');
|
||||
|
||||
cy.get('.chart-container').contains('No records found');
|
||||
|
||||
// Test column-specific search
|
||||
cy.get('.search-select').should('be.visible');
|
||||
|
||||
cy.get('.search-select').click();
|
||||
|
||||
cy.get('.ant-select-dropdown').should('be.visible');
|
||||
|
||||
cy.get('.ant-select-item-option').contains('state').should('be.visible');
|
||||
|
||||
cy.get('.ant-select-item-option').contains('state').click();
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').clear();
|
||||
|
||||
cy.get('span.dt-global-filter input.form-control.input-sm').type('CA');
|
||||
|
||||
cy.wait('@chartData');
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get('td[aria-labelledby="header-state"]').should('be.visible');
|
||||
|
||||
cy.get('td[aria-labelledby="header-state"]')
|
||||
.first()
|
||||
.should('contain', 'CA');
|
||||
});
|
||||
});
|
||||
|
||||
1208
superset-frontend/package-lock.json
generated
1208
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/xychart": "^3.5.1",
|
||||
"abortcontroller-polyfill": "^1.7.8",
|
||||
"ace-builds": "^1.36.3",
|
||||
"ace-builds": "^1.41.0",
|
||||
"ag-grid-community": "33.1.1",
|
||||
"ag-grid-react": "33.1.1",
|
||||
"antd": "4.10.3",
|
||||
@@ -137,6 +137,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dom-to-image-more": "^3.2.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"dompurify": "^3.2.4",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
"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-transform-modules-commonjs": "^7.26.3",
|
||||
"@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-typescript": "^7.26.0",
|
||||
"@babel/register": "^7.23.7",
|
||||
@@ -260,7 +261,6 @@
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/enzyme": "^3.10.18",
|
||||
"@types/fetch-mock": "^7.3.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jquery": "^3.5.8",
|
||||
@@ -290,9 +290,8 @@
|
||||
"@types/yargs": "12 - 18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
@@ -302,8 +301,6 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.2",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-matchers": "^7.1.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@types/enzyme": "^3.10.18",
|
||||
"@types/react": "*",
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.8.1"
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { ColumnMeta, SortSeriesData, SortSeriesType } from './types';
|
||||
|
||||
export const DEFAULT_MAX_ROW = 100000;
|
||||
export const DEFAULT_MAX_ROW_TABLE_SERVER = 500000;
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const TIME_FILTER_LABELS = {
|
||||
|
||||
@@ -30,7 +30,7 @@ export const aggregationOperator: PostProcessingFactory<
|
||||
> = (formData: QueryFormData, queryObject) => {
|
||||
const { aggregation = 'LAST_VALUE' } = formData;
|
||||
|
||||
if (aggregation === 'LAST_VALUE') {
|
||||
if (aggregation === 'LAST_VALUE' || aggregation === 'raw') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ export const aggregationControl = {
|
||||
clearable: false,
|
||||
renderTrigger: false,
|
||||
choices: [
|
||||
['raw', t('None')],
|
||||
['LAST_VALUE', t('Last Value')],
|
||||
['sum', t('Total (Sum)')],
|
||||
['mean', t('Average (Mean)')],
|
||||
@@ -77,7 +78,9 @@ export const aggregationControl = {
|
||||
['max', t('Maximum')],
|
||||
['median', t('Median')],
|
||||
],
|
||||
description: t('Select an aggregation method to apply to the metric.'),
|
||||
description: t(
|
||||
'Aggregation method used to compute the Big Number from the Trendline.For non-additive metrics like ratios, averages, distinct counts, etc use NONE.',
|
||||
),
|
||||
provideFormDataToProps: true,
|
||||
mapStateToProps: ({ form_data }: ControlPanelState) => ({
|
||||
value: form_data.aggregation || 'LAST_VALUE',
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
"@types/d3-scale": "^2.1.1",
|
||||
"@types/d3-time": "^3.0.4",
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/enzyme": "^3.10.18",
|
||||
"@types/fetch-mock": "^7.3.8",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface ChartMetadataConfig {
|
||||
label?: ChartLabel | null;
|
||||
labelExplanation?: string | null;
|
||||
queryObjectCount?: number;
|
||||
dynamicQueryObjectCount?: boolean;
|
||||
parseMethod?: ParseMethod;
|
||||
// suppressContextMenu: true hides the default context menu for the chart.
|
||||
// This is useful for viz plugins that define their own context menu.
|
||||
@@ -92,6 +93,8 @@ export default class ChartMetadata {
|
||||
|
||||
queryObjectCount: number;
|
||||
|
||||
dynamicQueryObjectCount: boolean;
|
||||
|
||||
parseMethod: ParseMethod;
|
||||
|
||||
suppressContextMenu?: boolean;
|
||||
@@ -115,6 +118,7 @@ export default class ChartMetadata {
|
||||
label = null,
|
||||
labelExplanation = null,
|
||||
queryObjectCount = 1,
|
||||
dynamicQueryObjectCount = false,
|
||||
parseMethod = 'json-bigint',
|
||||
suppressContextMenu = false,
|
||||
} = config;
|
||||
@@ -145,6 +149,7 @@ export default class ChartMetadata {
|
||||
this.label = label;
|
||||
this.labelExplanation = labelExplanation;
|
||||
this.queryObjectCount = queryObjectCount;
|
||||
this.dynamicQueryObjectCount = dynamicQueryObjectCount;
|
||||
this.parseMethod = parseMethod;
|
||||
this.suppressContextMenu = suppressContextMenu;
|
||||
}
|
||||
|
||||
@@ -58,17 +58,18 @@ export default async function parseResponse<T extends ParseMethod = 'json'>(
|
||||
const result: JsonResponse = {
|
||||
response,
|
||||
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 (
|
||||
value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
|
||||
value?.isLessThan?.(Number.MIN_SAFE_INTEGER)
|
||||
value?.isInteger?.() === true &&
|
||||
(value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
|
||||
value?.isLessThan?.(Number.MIN_SAFE_INTEGER))
|
||||
) {
|
||||
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;
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -25,3 +25,4 @@ export { default as validateNonEmpty } from './validateNonEmpty';
|
||||
export { default as validateMaxValue } from './validateMaxValue';
|
||||
export { default as validateMapboxStylesUrl } from './validateMapboxStylesUrl';
|
||||
export { default as validateTimeComparisonRangeValues } from './validateTimeComparisonRangeValues';
|
||||
export { default as validateServerPagination } from './validateServerPagination';
|
||||
|
||||
@@ -16,19 +16,23 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { t } from '../translation';
|
||||
|
||||
// taken from: https://github.com/enzymejs/enzyme/issues/2073
|
||||
// There is currently and issue with enzyme and react-16's hooks
|
||||
// that results in a race condition between tests and react hook updates.
|
||||
// This function ensures tests run after all react updates are done.
|
||||
export default async function waitForComponentToPaint<P = {}>(
|
||||
wrapper: ReactWrapper<P>,
|
||||
amount = 0,
|
||||
export default function validateServerPagination(
|
||||
v: unknown,
|
||||
serverPagination: boolean,
|
||||
maxValueWithoutServerPagination: number,
|
||||
maxServer: number,
|
||||
) {
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, amount));
|
||||
wrapper.update();
|
||||
});
|
||||
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 mockGetBigIntPayload = `{
|
||||
"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",
|
||||
"constructor": "constructor"
|
||||
}`;
|
||||
@@ -161,6 +161,7 @@ describe('parseResponse()', () => {
|
||||
expect(responseBigNumber.json.floatValue.minus).toEqual(
|
||||
-0.3452211361231223,
|
||||
);
|
||||
expect(responseBigNumber.json.floatValue.even).toEqual(1234567890123456);
|
||||
expect(
|
||||
responseBigNumber.json.floatValue.plus +
|
||||
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,11 +54,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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-typescript": "^7.23.3",
|
||||
"@storybook/react-webpack5": "8.2.9",
|
||||
"babel-loader": "^9.1.3",
|
||||
"babel-loader": "^10.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"ts-loader": "^9.5.2",
|
||||
"typescript": "^5.7.2"
|
||||
|
||||
@@ -38,9 +38,20 @@ import {
|
||||
} from '../DeckGLContainer';
|
||||
import { getExploreLongUrl } from '../utils/explore';
|
||||
import layerGenerators from '../layers';
|
||||
import { Viewport } from '../utils/fitViewport';
|
||||
import fitViewport, { Viewport } from '../utils/fitViewport';
|
||||
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 = {
|
||||
formData: QueryFormData;
|
||||
payload: JsonObject;
|
||||
@@ -56,7 +67,35 @@ export type DeckMultiProps = {
|
||||
const DeckMulti = (props: DeckMultiProps) => {
|
||||
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>>(
|
||||
{},
|
||||
);
|
||||
@@ -70,23 +109,31 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
|
||||
const loadLayers = useCallback(
|
||||
(formData: QueryFormData, payload: JsonObject, viewport?: Viewport) => {
|
||||
setViewport(viewport);
|
||||
setViewport(getAdjustedViewport());
|
||||
setSubSlicesLayers({});
|
||||
payload.data.slices.forEach(
|
||||
(subslice: { slice_id: number } & JsonObject) => {
|
||||
// Filters applied to multi_deck are passed down to underlying charts
|
||||
// note that dashboard contextual information (filter_immune_slices and such) aren't
|
||||
// taken into consideration here
|
||||
const filters = [
|
||||
...(subslice.form_data.filters || []),
|
||||
...(formData.filters || []),
|
||||
const extra_filters = [
|
||||
...(subslice.form_data.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 = {
|
||||
...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);
|
||||
@@ -136,7 +189,7 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
<DeckGLContainerStyledWrapper
|
||||
ref={containerRef}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport || props.viewport}
|
||||
viewport={viewport}
|
||||
layers={layers}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
|
||||
@@ -29,7 +29,7 @@ import TooltipRow from '../../TooltipRow';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
|
||||
function getPoints(data: JsonObject[]) {
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
const points: Point[] = [];
|
||||
data.forEach(d => {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import { commonLayerProps } from '../common';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
|
||||
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
|
||||
properties: JsonObject;
|
||||
@@ -172,6 +173,17 @@ export type DeckGLGeoJsonProps = {
|
||||
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 containerRef = useRef<DeckGLContainerHandle>();
|
||||
const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
|
||||
@@ -186,24 +198,13 @@ const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||
|
||||
const viewport: Viewport = useMemo(() => {
|
||||
if (formData.autozoom) {
|
||||
const points =
|
||||
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;
|
||||
},
|
||||
[],
|
||||
) || [];
|
||||
const points = getPoints(payload.data.features) || [];
|
||||
|
||||
if (points.length) {
|
||||
return fitViewport(props.viewport, {
|
||||
width,
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export const getLayer: getLayerType<unknown> = (
|
||||
});
|
||||
};
|
||||
|
||||
function getPoints(data: any[]) {
|
||||
export function getPoints(data: any[]) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export function getLayer(
|
||||
});
|
||||
}
|
||||
|
||||
function getPoints(data: JsonObject[]) {
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
let points: Point[] = [];
|
||||
data.forEach(d => {
|
||||
points = points.concat(d.path);
|
||||
|
||||
@@ -173,6 +173,10 @@ export type DeckGLPolygonProps = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.flatMap(getPointsFromPolygon);
|
||||
}
|
||||
|
||||
const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
const containerRef = useRef<DeckGLContainerHandle>();
|
||||
|
||||
@@ -183,7 +187,7 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
viewport = fitViewport(viewport, {
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
points: features.flatMap(getPointsFromPolygon),
|
||||
points: getPoints(features),
|
||||
});
|
||||
}
|
||||
if (viewport.zoom < 0) {
|
||||
|
||||
@@ -30,7 +30,7 @@ import TooltipRow from '../../TooltipRow';
|
||||
import { unitToRadius } from '../../utils/geo';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
|
||||
function getPoints(data: JsonObject[]) {
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from '../../DeckGLContainer';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
|
||||
function getPoints(data: JsonObject[]) {
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-tip": "^0.9.1",
|
||||
"dompurify": "^3.2.4",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"dayjs": "^1.11.13",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"dompurify": "^3.2.4",
|
||||
"prop-types": "^15.8.1",
|
||||
"urijs": "^1.19.11"
|
||||
},
|
||||
|
||||
@@ -36,13 +36,25 @@ import {
|
||||
} from './types';
|
||||
import { useOverflowDetection } from './useOverflowDetection';
|
||||
|
||||
const MetricNameText = styled.div<{ metricNameFontSize?: number }>`
|
||||
${({ theme, metricNameFontSize }) => `
|
||||
font-family: ${theme.typography.families.sansSerif};
|
||||
font-weight: ${theme.typography.weights.normal};
|
||||
font-size: ${metricNameFontSize || theme.typography.sizes.s * 2}px;
|
||||
text-align: center;
|
||||
margin-bottom: ${theme.gridUnit * 3}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
const NumbersContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
`;
|
||||
|
||||
const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
|
||||
@@ -73,6 +85,8 @@ export default function PopKPI(props: PopKPIProps) {
|
||||
prevNumber,
|
||||
valueDifference,
|
||||
percentDifferenceFormattedString,
|
||||
metricName,
|
||||
metricNameFontSize,
|
||||
headerFontSize,
|
||||
subheaderFontSize,
|
||||
comparisonColorEnabled,
|
||||
@@ -84,8 +98,8 @@ export default function PopKPI(props: PopKPIProps) {
|
||||
subtitle,
|
||||
subtitleFontSize,
|
||||
dashboardTimeRange,
|
||||
showMetricName,
|
||||
} = props;
|
||||
|
||||
const [comparisonRange, setComparisonRange] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -260,9 +274,16 @@ export default function PopKPI(props: PopKPIProps) {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
align-items: flex-start;
|
||||
overflow: auto;
|
||||
`
|
||||
}
|
||||
>
|
||||
{showMetricName && metricName && (
|
||||
<MetricNameText metricNameFontSize={metricNameFontSize}>
|
||||
{metricName}
|
||||
</MetricNameText>
|
||||
)}
|
||||
|
||||
<div css={bigValueContainerStyles}>
|
||||
{bigNumber}
|
||||
{percentDifferenceNumber !== 0 && (
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
subheaderFontSize,
|
||||
subtitleControl,
|
||||
subtitleFontSize,
|
||||
showMetricNameControl,
|
||||
metricNameFontSizeWithVisibility,
|
||||
} from '../sharedControls';
|
||||
import { ColorSchemeEnum } from './types';
|
||||
|
||||
@@ -70,6 +72,8 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
[subtitleControl],
|
||||
[subtitleFontSize],
|
||||
[showMetricNameControl],
|
||||
[metricNameFontSizeWithVisibility],
|
||||
[
|
||||
{
|
||||
...subheaderFontSize,
|
||||
|
||||
@@ -32,6 +32,7 @@ export default class PopKPIPlugin extends ChartPlugin {
|
||||
tags: [
|
||||
t('Comparison'),
|
||||
t('Business'),
|
||||
t('ECharts'),
|
||||
t('Percentages'),
|
||||
t('Report'),
|
||||
t('Advanced-Analytics'),
|
||||
|
||||
@@ -26,7 +26,13 @@ import {
|
||||
SimpleAdhocFilter,
|
||||
ensureIsArray,
|
||||
} from '@superset-ui/core';
|
||||
import { getComparisonFontSize, getHeaderFontSize } from './utils';
|
||||
import {
|
||||
getComparisonFontSize,
|
||||
getHeaderFontSize,
|
||||
getMetricNameFontSize,
|
||||
} from './utils';
|
||||
|
||||
import { getOriginalLabel } from '../utils';
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
@@ -83,6 +89,7 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
headerFontSize,
|
||||
headerText,
|
||||
metric,
|
||||
metricNameFontSize,
|
||||
yAxisFormat,
|
||||
currencyFormat,
|
||||
subheaderFontSize,
|
||||
@@ -91,11 +98,14 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
percentDifferenceFormat,
|
||||
subtitle = '',
|
||||
subtitleFontSize,
|
||||
columnConfig,
|
||||
columnConfig = {},
|
||||
} = formData;
|
||||
const { data: dataA = [] } = queriesData[0];
|
||||
const data = dataA;
|
||||
const metricName = metric ? getMetricLabel(metric) : '';
|
||||
const metrics = chartProps.datasource?.metrics || [];
|
||||
const originalLabel = getOriginalLabel(metric, metrics);
|
||||
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
||||
const timeComparison = ensureIsArray(chartProps.rawFormData?.time_compare)[0];
|
||||
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
||||
const currentTimeRangeFilter = chartProps.rawFormData?.adhoc_filters?.filter(
|
||||
@@ -179,7 +189,7 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
metricName,
|
||||
metricName: originalLabel,
|
||||
bigNumber,
|
||||
prevNumber,
|
||||
valueDifference,
|
||||
@@ -187,6 +197,8 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
boldText,
|
||||
subtitle,
|
||||
subtitleFontSize,
|
||||
showMetricName,
|
||||
metricNameFontSize: getMetricNameFontSize(metricNameFontSize),
|
||||
headerFontSize: getHeaderFontSize(headerFontSize),
|
||||
subheaderFontSize: getComparisonFontSize(subheaderFontSize),
|
||||
headerText,
|
||||
|
||||
@@ -61,6 +61,8 @@ export type PopKPIProps = PopKPIStylesProps &
|
||||
data: TimeseriesDataRecord[];
|
||||
metrics: Metric[];
|
||||
metricName: string;
|
||||
metricNameFontSize?: number;
|
||||
showMetricName: boolean;
|
||||
bigNumber: string;
|
||||
prevNumber: string;
|
||||
subtitle?: string;
|
||||
|
||||
@@ -16,10 +16,19 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { headerFontSize, subheaderFontSize } from '../sharedControls';
|
||||
import {
|
||||
headerFontSize,
|
||||
subheaderFontSize,
|
||||
metricNameFontSize,
|
||||
} from '../sharedControls';
|
||||
|
||||
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 =
|
||||
headerFontSize.config.options.map(
|
||||
@@ -40,6 +49,10 @@ const getFontSizeMapping = (
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const metricNameFontSizesMapping = getFontSizeMapping(
|
||||
metricNameProportionValues,
|
||||
sharedFontSizes,
|
||||
);
|
||||
const headerFontSizesMapping = getFontSizeMapping(
|
||||
headerProportionValues,
|
||||
headerFontSizes,
|
||||
@@ -47,13 +60,17 @@ const headerFontSizesMapping = getFontSizeMapping(
|
||||
|
||||
const comparisonFontSizesMapping = getFontSizeMapping(
|
||||
subheaderProportionValues,
|
||||
comparisonFontSizes,
|
||||
sharedFontSizes,
|
||||
);
|
||||
|
||||
export const getMetricNameFontSize = (proportionValue: number) =>
|
||||
metricNameFontSizesMapping[proportionValue] ??
|
||||
sharedFontSizes[sharedFontSizes.length - 1];
|
||||
|
||||
export const getHeaderFontSize = (proportionValue: number) =>
|
||||
headerFontSizesMapping[proportionValue] ??
|
||||
headerFontSizes[headerFontSizes.length - 1];
|
||||
|
||||
export const getComparisonFontSize = (proportionValue: number) =>
|
||||
comparisonFontSizesMapping[proportionValue] ??
|
||||
comparisonFontSizes[comparisonFontSizes.length - 1];
|
||||
sharedFontSizes[sharedFontSizes.length - 1];
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
headerFontSize,
|
||||
subtitleFontSize,
|
||||
subtitleControl,
|
||||
showMetricNameControl,
|
||||
metricNameFontSizeWithVisibility,
|
||||
} from '../sharedControls';
|
||||
|
||||
export default {
|
||||
@@ -44,6 +46,8 @@ export default {
|
||||
[headerFontSize],
|
||||
[subtitleControl],
|
||||
[subtitleFontSize],
|
||||
[showMetricNameControl],
|
||||
[metricNameFontSizeWithVisibility],
|
||||
['y_axis_format'],
|
||||
['currency_format'],
|
||||
[
|
||||
|
||||
@@ -39,6 +39,7 @@ const metadata = {
|
||||
tags: [
|
||||
t('Additive'),
|
||||
t('Business'),
|
||||
t('ECharts'),
|
||||
t('Legacy'),
|
||||
t('Percentages'),
|
||||
t('Featured'),
|
||||
|
||||
@@ -36,6 +36,7 @@ jest.mock('@superset-ui/core', () => ({
|
||||
jest.mock('../utils', () => ({
|
||||
getDateFormatter: jest.fn(() => (v: any) => `${v}pm`),
|
||||
parseMetricValue: jest.fn(val => Number(val)),
|
||||
getOriginalLabel: jest.fn((metric, metrics) => metric),
|
||||
}));
|
||||
|
||||
describe('BigNumberTotal transformProps', () => {
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
getValueFormatter,
|
||||
} from '@superset-ui/core';
|
||||
import { BigNumberTotalChartProps, BigNumberVizProps } from '../types';
|
||||
import { getDateFormatter, parseMetricValue } from '../utils';
|
||||
import { getDateFormatter, getOriginalLabel, parseMetricValue } from '../utils';
|
||||
import { Refs } from '../../types';
|
||||
|
||||
export default function transformProps(
|
||||
@@ -45,6 +45,7 @@ export default function transformProps(
|
||||
datasource: { currencyFormats = {}, columnFormats = {} },
|
||||
} = chartProps;
|
||||
const {
|
||||
metricNameFontSize,
|
||||
headerFontSize,
|
||||
metric = 'value',
|
||||
subtitle,
|
||||
@@ -58,9 +59,12 @@ export default function transformProps(
|
||||
subheaderFontSize,
|
||||
} = formData;
|
||||
const refs: Refs = {};
|
||||
const { data = [], coltypes = [] } = queriesData[0];
|
||||
const { data = [], coltypes = [] } = queriesData[0] || {};
|
||||
const granularity = extractTimegrain(rawFormData as QueryFormData);
|
||||
const metrics = chartProps.datasource?.metrics || [];
|
||||
const originalLabel = getOriginalLabel(metric, metrics);
|
||||
const metricName = getMetricLabel(metric);
|
||||
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
||||
const formattedSubtitle = subtitle?.trim() ? subtitle : subheader || '';
|
||||
const formattedSubtitleFontSize = subtitle?.trim()
|
||||
? (subtitleFontSize ?? 1)
|
||||
@@ -103,7 +107,6 @@ export default function transformProps(
|
||||
const colorThresholdFormatters =
|
||||
getColorFormatters(conditionalFormatting, data, false) ??
|
||||
defaultColorFormatters;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
@@ -116,5 +119,8 @@ export default function transformProps(
|
||||
onContextMenu,
|
||||
refs,
|
||||
colorThresholdFormatters,
|
||||
metricName: originalLabel,
|
||||
showMetricName,
|
||||
metricNameFontSize,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { PureComponent, MouseEvent } from 'react';
|
||||
import { PureComponent, MouseEvent, createRef } from 'react';
|
||||
import {
|
||||
t,
|
||||
getNumberFormatter,
|
||||
@@ -35,6 +35,7 @@ const defaultNumberFormatter = getNumberFormatter();
|
||||
|
||||
const PROPORTION = {
|
||||
// text size: proportion of the chart container sans trendline
|
||||
METRIC_NAME: 0.125,
|
||||
KICKER: 0.1,
|
||||
HEADER: 0.3,
|
||||
SUBHEADER: 0.125,
|
||||
@@ -42,13 +43,20 @@ const PROPORTION = {
|
||||
TRENDLINE: 0.3,
|
||||
};
|
||||
|
||||
class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
type BigNumberVisState = {
|
||||
elementsRendered: boolean;
|
||||
recalculateTrigger: boolean;
|
||||
};
|
||||
|
||||
class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
headerFormatter: defaultNumberFormatter,
|
||||
formatTime: getTimeFormatter(SMART_DATE_VERBOSE_ID),
|
||||
headerFontSize: PROPORTION.HEADER,
|
||||
kickerFontSize: PROPORTION.KICKER,
|
||||
metricNameFontSize: PROPORTION.METRIC_NAME,
|
||||
showMetricName: true,
|
||||
mainColor: BRAND_COLOR,
|
||||
showTimestamp: false,
|
||||
showTrendLine: false,
|
||||
@@ -58,6 +66,40 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
timeRangeFixed: false,
|
||||
};
|
||||
|
||||
// Create refs for each component to measure heights
|
||||
metricNameRef = createRef<HTMLDivElement>();
|
||||
|
||||
kickerRef = createRef<HTMLDivElement>();
|
||||
|
||||
headerRef = createRef<HTMLDivElement>();
|
||||
|
||||
subheaderRef = createRef<HTMLDivElement>();
|
||||
|
||||
subtitleRef = createRef<HTMLDivElement>();
|
||||
|
||||
state = {
|
||||
elementsRendered: false,
|
||||
recalculateTrigger: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// Wait for elements to render and then calculate heights
|
||||
setTimeout(() => {
|
||||
this.setState({ elementsRendered: true });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: BigNumberVizProps) {
|
||||
if (
|
||||
prevProps.height !== this.props.height ||
|
||||
prevProps.showTrendLine !== this.props.showTrendLine
|
||||
) {
|
||||
this.setState(prevState => ({
|
||||
recalculateTrigger: !prevState.recalculateTrigger,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
getClassName() {
|
||||
const { className, showTrendLine, bigNumberFallback } = this.props;
|
||||
const names = `superset-legacy-chart-big-number ${className} ${
|
||||
@@ -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) {
|
||||
const { timestamp, showTimestamp, formatTime, width } = this.props;
|
||||
if (
|
||||
@@ -118,6 +191,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.kickerRef}
|
||||
className="kicker"
|
||||
style={{
|
||||
fontSize,
|
||||
@@ -173,6 +247,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.headerRef}
|
||||
className="header-line"
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -211,6 +286,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.subheaderRef}
|
||||
className="subheader-line"
|
||||
style={{
|
||||
fontSize,
|
||||
@@ -256,6 +332,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={this.subtitleRef}
|
||||
className="subtitle-line subheader-line"
|
||||
style={{
|
||||
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() {
|
||||
const {
|
||||
showTrendLine,
|
||||
@@ -323,6 +429,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
kickerFontSize,
|
||||
headerFontSize,
|
||||
subtitleFontSize,
|
||||
metricNameFontSize,
|
||||
subheaderFontSize,
|
||||
} = this.props;
|
||||
const className = this.getClassName();
|
||||
@@ -330,11 +437,31 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
if (showTrendLine) {
|
||||
const chartHeight = Math.floor(PROPORTION.TRENDLINE * height);
|
||||
const allTextHeight = height - chartHeight;
|
||||
const shouldApplyOverflow = this.shouldApplyOverflow(allTextHeight);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="text-container" style={{ height: allTextHeight }}>
|
||||
<div
|
||||
className="text-container"
|
||||
style={{
|
||||
height: allTextHeight,
|
||||
...(shouldApplyOverflow
|
||||
? {
|
||||
display: 'block',
|
||||
boxSizing: 'border-box',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{this.renderFallbackWarning()}
|
||||
{this.renderMetricName(
|
||||
Math.ceil(
|
||||
(metricNameFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
|
||||
),
|
||||
)}
|
||||
{this.renderKicker(
|
||||
Math.ceil(
|
||||
(kickerFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
|
||||
@@ -356,16 +483,33 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const shouldApplyOverflow = this.shouldApplyOverflow(height);
|
||||
return (
|
||||
<div className={className} style={{ height }}>
|
||||
{this.renderFallbackWarning()}
|
||||
{this.renderKicker((kickerFontSize || 0) * height)}
|
||||
{this.renderHeader(Math.ceil(headerFontSize * height))}
|
||||
{this.rendermetricComparisonSummary(
|
||||
Math.ceil(subheaderFontSize * height),
|
||||
)}
|
||||
{this.renderSubtitle(Math.ceil(subtitleFontSize * height))}
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
height,
|
||||
...(shouldApplyOverflow
|
||||
? {
|
||||
display: 'block',
|
||||
boxSizing: 'border-box',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<div className="text-container">
|
||||
{this.renderFallbackWarning()}
|
||||
{this.renderMetricName((metricNameFontSize || 0) * height)}
|
||||
{this.renderKicker((kickerFontSize || 0) * height)}
|
||||
{this.renderHeader(Math.ceil(headerFontSize * height))}
|
||||
{this.rendermetricComparisonSummary(
|
||||
Math.ceil(subheaderFontSize * height),
|
||||
)}
|
||||
{this.renderSubtitle(Math.ceil(subtitleFontSize * height))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -400,7 +544,12 @@ export default styled(BigNumberVis)`
|
||||
|
||||
.kicker {
|
||||
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 {
|
||||
@@ -416,12 +565,12 @@ export default styled(BigNumberVis)`
|
||||
|
||||
.subheader-line {
|
||||
line-height: 1em;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
|
||||
.subtitle-line {
|
||||
line-height: 1em;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
|
||||
&.is-fallback-value {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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 { QueryFormData } from '@superset-ui/core';
|
||||
import buildQuery from './buildQuery';
|
||||
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
...jest.requireActual('@superset-ui/core'),
|
||||
getXAxisColumn: jest.fn(() => 'order_date'),
|
||||
isXAxisSet: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
jest.mock('@superset-ui/chart-controls', () => ({
|
||||
pivotOperator: jest.fn(() => ({ operation: 'pivot' })),
|
||||
aggregationOperator: jest.fn(formData => {
|
||||
if (formData.aggregation === 'LAST_VALUE' || !formData.aggregation) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
operation: 'aggregation',
|
||||
options: { operator: formData.aggregation },
|
||||
};
|
||||
}),
|
||||
flattenOperator: jest.fn(() => ({ operation: 'flatten' })),
|
||||
resampleOperator: jest.fn(() => ({ operation: 'resample' })),
|
||||
rollingWindowOperator: jest.fn(() => ({ operation: 'rolling' })),
|
||||
}));
|
||||
|
||||
describe('BigNumberWithTrendline buildQuery', () => {
|
||||
const baseFormData: QueryFormData = {
|
||||
datasource: '1__table',
|
||||
viz_type: 'big_number',
|
||||
metric: 'custom_metric',
|
||||
aggregation: null,
|
||||
};
|
||||
|
||||
it('creates raw metric query when aggregation is null', () => {
|
||||
const queryContext = buildQuery({ ...baseFormData });
|
||||
const bigNumberQuery = queryContext.queries[1];
|
||||
|
||||
expect(bigNumberQuery.post_processing).toEqual([{ operation: 'pivot' }]);
|
||||
expect(bigNumberQuery.is_timeseries).toBe(true);
|
||||
});
|
||||
|
||||
it('adds aggregation operator when aggregation is "sum"', () => {
|
||||
const queryContext = buildQuery({ ...baseFormData, aggregation: 'sum' });
|
||||
const bigNumberQuery = queryContext.queries[1];
|
||||
|
||||
expect(bigNumberQuery.post_processing).toEqual([
|
||||
{ operation: 'pivot' },
|
||||
{ operation: 'aggregation', options: { operator: 'sum' } },
|
||||
]);
|
||||
expect(bigNumberQuery.is_timeseries).toBe(true);
|
||||
});
|
||||
|
||||
it('skips aggregation when aggregation is LAST_VALUE', () => {
|
||||
const queryContext = buildQuery({
|
||||
...baseFormData,
|
||||
aggregation: 'LAST_VALUE',
|
||||
});
|
||||
const bigNumberQuery = queryContext.queries[1];
|
||||
|
||||
expect(bigNumberQuery.post_processing).toEqual([{ operation: 'pivot' }]);
|
||||
expect(bigNumberQuery.is_timeseries).toBe(true);
|
||||
});
|
||||
|
||||
it('always returns two queries', () => {
|
||||
const queryContext = buildQuery({ ...baseFormData });
|
||||
expect(queryContext.queries.length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
buildQueryContext,
|
||||
ensureIsArray,
|
||||
@@ -32,15 +33,17 @@ import {
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
const isRawMetric = formData.aggregation === 'raw';
|
||||
|
||||
const timeColumn = isXAxisSet(formData)
|
||||
? ensureIsArray(getXAxisColumn(formData))
|
||||
: [];
|
||||
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
columns: [
|
||||
...(isXAxisSet(formData)
|
||||
? ensureIsArray(getXAxisColumn(formData))
|
||||
: []),
|
||||
],
|
||||
...(isXAxisSet(formData) ? {} : { is_timeseries: true }),
|
||||
columns: [...timeColumn],
|
||||
...(timeColumn.length ? {} : { is_timeseries: true }),
|
||||
post_processing: [
|
||||
pivotOperator(formData, baseQueryObject),
|
||||
rollingWindowOperator(formData, baseQueryObject),
|
||||
@@ -48,19 +51,16 @@ export default function buildQuery(formData: QueryFormData) {
|
||||
flattenOperator(formData, baseQueryObject),
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
...baseQueryObject,
|
||||
columns: [
|
||||
...(isXAxisSet(formData)
|
||||
? ensureIsArray(getXAxisColumn(formData))
|
||||
: []),
|
||||
],
|
||||
...(isXAxisSet(formData) ? {} : { is_timeseries: true }),
|
||||
post_processing: [
|
||||
pivotOperator(formData, baseQueryObject),
|
||||
aggregationOperator(formData, baseQueryObject),
|
||||
],
|
||||
columns: [...(isRawMetric ? [] : timeColumn)],
|
||||
is_timeseries: !isRawMetric,
|
||||
post_processing: isRawMetric
|
||||
? []
|
||||
: [
|
||||
pivotOperator(formData, baseQueryObject),
|
||||
aggregationOperator(formData, baseQueryObject),
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
subheaderFontSize,
|
||||
subtitleFontSize,
|
||||
subtitleControl,
|
||||
showMetricNameControl,
|
||||
metricNameFontSizeWithVisibility,
|
||||
} from '../sharedControls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -141,6 +143,8 @@ const config: ControlPanelConfig = {
|
||||
[subheaderFontSize],
|
||||
[subtitleControl],
|
||||
[subtitleFontSize],
|
||||
[showMetricNameControl],
|
||||
[metricNameFontSizeWithVisibility],
|
||||
['y_axis_format'],
|
||||
['currency_format'],
|
||||
[
|
||||
|
||||
@@ -37,6 +37,7 @@ const metadata = {
|
||||
name: t('Big Number with Trendline'),
|
||||
tags: [
|
||||
t('Advanced-Analytics'),
|
||||
t('ECharts'),
|
||||
t('Line'),
|
||||
t('Percentages'),
|
||||
t('Featured'),
|
||||
|
||||
@@ -39,6 +39,7 @@ jest.mock('@superset-ui/core', () => ({
|
||||
jest.mock('../utils', () => ({
|
||||
getDateFormatter: jest.fn(() => (v: any) => `${v}pm`),
|
||||
parseMetricValue: jest.fn(val => Number(val)),
|
||||
getOriginalLabel: jest.fn((metric, metrics) => metric),
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/tooltip', () => ({
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
BigNumberWithTrendlineChartProps,
|
||||
TimeSeriesDatum,
|
||||
} from '../types';
|
||||
import { getDateFormatter, parseMetricValue } from '../utils';
|
||||
import { getDateFormatter, parseMetricValue, getOriginalLabel } from '../utils';
|
||||
import { getDefaultTooltip } from '../../utils/tooltip';
|
||||
import { Refs } from '../../types';
|
||||
|
||||
@@ -62,6 +62,7 @@ export default function transformProps(
|
||||
compareLag: compareLag_,
|
||||
compareSuffix = '',
|
||||
timeFormat,
|
||||
metricNameFontSize,
|
||||
headerFontSize,
|
||||
metric = 'value',
|
||||
showTimestamp,
|
||||
@@ -96,6 +97,9 @@ export default function transformProps(
|
||||
const aggregatedData = hasAggregatedData ? aggregatedQueryData.data[0] : null;
|
||||
const refs: Refs = {};
|
||||
const metricName = getMetricLabel(metric);
|
||||
const metrics = chartProps.datasource?.metrics || [];
|
||||
const originalLabel = getOriginalLabel(metric, metrics);
|
||||
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
||||
const compareLag = Number(compareLag_) || 0;
|
||||
let formattedSubheader = subheader;
|
||||
|
||||
@@ -303,6 +307,9 @@ export default function transformProps(
|
||||
headerFormatter,
|
||||
formatTime,
|
||||
formData,
|
||||
metricName: originalLabel,
|
||||
showMetricName,
|
||||
metricNameFontSize,
|
||||
headerFontSize,
|
||||
subtitleFontSize,
|
||||
subtitle,
|
||||
|
||||
@@ -21,106 +21,68 @@
|
||||
import { t } from '@superset-ui/core';
|
||||
import { CustomControlItem } from '@superset-ui/chart-controls';
|
||||
|
||||
export const headerFontSize: CustomControlItem = {
|
||||
name: 'header_font_size',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Big Number Font Size'),
|
||||
renderTrigger: true,
|
||||
clearable: false,
|
||||
default: 0.4,
|
||||
// Values represent the percentage of space a header should take
|
||||
options: [
|
||||
{
|
||||
label: t('Tiny'),
|
||||
value: 0.2,
|
||||
},
|
||||
{
|
||||
label: t('Small'),
|
||||
value: 0.3,
|
||||
},
|
||||
{
|
||||
label: t('Normal'),
|
||||
value: 0.4,
|
||||
},
|
||||
{
|
||||
label: t('Large'),
|
||||
value: 0.5,
|
||||
},
|
||||
{
|
||||
label: t('Huge'),
|
||||
value: 0.6,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const FONT_SIZE_OPTIONS_SMALL = [
|
||||
{ label: t('Tiny'), value: 0.125 },
|
||||
{ label: t('Small'), value: 0.15 },
|
||||
{ label: t('Normal'), value: 0.2 },
|
||||
{ label: t('Large'), value: 0.3 },
|
||||
{ label: t('Huge'), value: 0.4 },
|
||||
];
|
||||
|
||||
export const subtitleFontSize: CustomControlItem = {
|
||||
name: 'subtitle_font_size',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subtitle Font Size'),
|
||||
renderTrigger: true,
|
||||
clearable: false,
|
||||
default: 0.15,
|
||||
// Values represent the percentage of space a subtitle should take
|
||||
options: [
|
||||
{
|
||||
label: t('Tiny'),
|
||||
value: 0.125,
|
||||
},
|
||||
{
|
||||
label: t('Small'),
|
||||
value: 0.15,
|
||||
},
|
||||
{
|
||||
label: t('Normal'),
|
||||
value: 0.2,
|
||||
},
|
||||
{
|
||||
label: t('Large'),
|
||||
value: 0.3,
|
||||
},
|
||||
{
|
||||
label: t('Huge'),
|
||||
value: 0.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
export const subheaderFontSize: CustomControlItem = {
|
||||
name: 'subheader_font_size',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subheader Font Size'),
|
||||
renderTrigger: true,
|
||||
clearable: false,
|
||||
default: 0.15,
|
||||
// Values represent the percentage of space a subheader should take
|
||||
options: [
|
||||
{
|
||||
label: t('Tiny'),
|
||||
value: 0.125,
|
||||
},
|
||||
{
|
||||
label: t('Small'),
|
||||
value: 0.15,
|
||||
},
|
||||
{
|
||||
label: t('Normal'),
|
||||
value: 0.2,
|
||||
},
|
||||
{
|
||||
label: t('Large'),
|
||||
value: 0.3,
|
||||
},
|
||||
{
|
||||
label: t('Huge'),
|
||||
value: 0.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const FONT_SIZE_OPTIONS_LARGE = [
|
||||
{ label: t('Tiny'), value: 0.2 },
|
||||
{ label: t('Small'), value: 0.3 },
|
||||
{ label: t('Normal'), value: 0.4 },
|
||||
{ label: t('Large'), value: 0.5 },
|
||||
{ label: t('Huge'), value: 0.6 },
|
||||
];
|
||||
|
||||
function makeFontSizeControl(
|
||||
name: string,
|
||||
label: string,
|
||||
defaultValue: number,
|
||||
options: { label: string; value: number }[],
|
||||
): CustomControlItem {
|
||||
return {
|
||||
name,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t(label),
|
||||
renderTrigger: true,
|
||||
clearable: false,
|
||||
default: defaultValue,
|
||||
options,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const headerFontSize = makeFontSizeControl(
|
||||
'header_font_size',
|
||||
'Big Number Font Size',
|
||||
0.4,
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
);
|
||||
|
||||
export const subtitleFontSize = makeFontSizeControl(
|
||||
'subtitle_font_size',
|
||||
'Subtitle Font Size',
|
||||
0.15,
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
);
|
||||
|
||||
export const subheaderFontSize = makeFontSizeControl(
|
||||
'subheader_font_size',
|
||||
'Subheader Font Size',
|
||||
0.15,
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
);
|
||||
|
||||
export const metricNameFontSize = makeFontSizeControl(
|
||||
'metric_name_font_size',
|
||||
'Metric Name Font Size',
|
||||
0.15,
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
);
|
||||
|
||||
export const subtitleControl: CustomControlItem = {
|
||||
name: 'subtitle',
|
||||
@@ -131,3 +93,23 @@ export const subtitleControl: CustomControlItem = {
|
||||
description: t('Description text that shows up below your Big Number'),
|
||||
},
|
||||
};
|
||||
|
||||
export const showMetricNameControl: CustomControlItem = {
|
||||
name: 'show_metric_name',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Metric Name'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t('Whether to display the metric name'),
|
||||
},
|
||||
};
|
||||
|
||||
export const metricNameFontSizeWithVisibility: CustomControlItem = {
|
||||
...metricNameFontSize,
|
||||
config: {
|
||||
...metricNameFontSize.config,
|
||||
visibility: ({ controls }) => controls?.show_metric_name?.value === true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -75,6 +75,10 @@ export type BigNumberVizProps = {
|
||||
bigNumberFallback?: TimeSeriesDatum;
|
||||
headerFormatter: ValueFormatter | TimeFormatter;
|
||||
formatTime?: TimeFormatter;
|
||||
metricName?: string;
|
||||
friendlyMetricName?: string;
|
||||
metricNameFontSize?: number;
|
||||
showMetricName?: boolean;
|
||||
headerFontSize: number;
|
||||
kickerFontSize?: number;
|
||||
subheader?: string;
|
||||
|
||||
@@ -22,6 +22,10 @@ import utc from 'dayjs/plugin/utc';
|
||||
import {
|
||||
getTimeFormatter,
|
||||
getTimeFormatterForGranularity,
|
||||
isAdhocMetricSimple,
|
||||
isSavedMetric,
|
||||
Metric,
|
||||
QueryFormMetric,
|
||||
SMART_DATE_ID,
|
||||
TimeGranularity,
|
||||
} from '@superset-ui/core';
|
||||
@@ -47,3 +51,43 @@ export const getDateFormatter = (
|
||||
timeFormat === SMART_DATE_ID
|
||||
? getTimeFormatterForGranularity(granularity)
|
||||
: getTimeFormatter(timeFormat ?? fallbackFormat);
|
||||
|
||||
export function getOriginalLabel(
|
||||
metric: QueryFormMetric,
|
||||
metrics: Metric[] = [],
|
||||
): string {
|
||||
const metricLabel = typeof metric === 'string' ? metric : metric.label || '';
|
||||
|
||||
if (isSavedMetric(metric)) {
|
||||
const metricEntry = metrics.find(m => m.metric_name === metric);
|
||||
return (
|
||||
metricEntry?.verbose_name ||
|
||||
metricEntry?.metric_name ||
|
||||
metric ||
|
||||
'Unknown Metric'
|
||||
);
|
||||
}
|
||||
|
||||
if (isAdhocMetricSimple(metric)) {
|
||||
const column = metric.column || {};
|
||||
const columnName = column.column_name || 'unknown_column';
|
||||
const verboseName = column.verbose_name || columnName;
|
||||
const aggregate = metric.aggregate || 'UNKNOWN';
|
||||
return metric.hasCustomLabel && metric.label
|
||||
? metric.label
|
||||
: `${aggregate}(${verboseName})`;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof metric === 'object' &&
|
||||
'expressionType' in metric &&
|
||||
metric.expressionType === 'SQL' &&
|
||||
'sqlExpression' in metric
|
||||
) {
|
||||
return metric.hasCustomLabel && metric.label
|
||||
? metric.label
|
||||
: metricLabel || 'Custom Metric';
|
||||
}
|
||||
|
||||
return metricLabel || 'Unknown Metric';
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { ensureIsArray, t } from '@superset-ui/core';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import {
|
||||
ControlPanelsContainerProps,
|
||||
ControlPanelConfig,
|
||||
ControlPanelSectionConfig,
|
||||
ControlSetRow,
|
||||
@@ -27,6 +28,8 @@ import {
|
||||
getStandardizedControls,
|
||||
sections,
|
||||
sharedControls,
|
||||
DEFAULT_SORT_SERIES_DATA,
|
||||
SORT_SERIES_CHOICES,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
@@ -196,6 +199,23 @@ function createCustomizeSection(
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: `only_total${controlSuffix}`,
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Only Total'),
|
||||
default: true,
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Only show the total value on the stacked chart, and not show on the selected category',
|
||||
),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
Boolean(controls?.show_value?.value) &&
|
||||
Boolean(controls?.stack?.value),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: `opacity${controlSuffix}`,
|
||||
@@ -258,6 +278,35 @@ function createCustomizeSection(
|
||||
},
|
||||
},
|
||||
],
|
||||
[<ControlSubSectionHeader>{t('Series Order')}</ControlSubSectionHeader>],
|
||||
[
|
||||
{
|
||||
name: `sort_series_type${controlSuffix}`,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: false,
|
||||
label: t('Sort Series By'),
|
||||
choices: SORT_SERIES_CHOICES,
|
||||
default: DEFAULT_SORT_SERIES_DATA.sort_series_type,
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Based on what should series be ordered on the chart and legend',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: `sort_series_ascending${controlSuffix}`,
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Sort Series Ascending'),
|
||||
default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending,
|
||||
renderTrigger: true,
|
||||
description: t('Sort series in ascending order'),
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
getXAxisFormatter,
|
||||
getYAxisFormatter,
|
||||
} from '../utils/formatters';
|
||||
import { getMetricDisplayName } from '../utils/metricDisplayName';
|
||||
|
||||
const getFormatter = (
|
||||
customFormatters: Record<string, ValueFormatter>,
|
||||
@@ -172,6 +173,8 @@ export default function transformProps(
|
||||
showLegend,
|
||||
showValue,
|
||||
showValueB,
|
||||
onlyTotal,
|
||||
onlyTotalB,
|
||||
stack,
|
||||
stackB,
|
||||
truncateXAxis,
|
||||
@@ -202,6 +205,10 @@ export default function transformProps(
|
||||
yAxisTitleMargin,
|
||||
yAxisTitlePosition,
|
||||
sliceId,
|
||||
sortSeriesType,
|
||||
sortSeriesTypeB,
|
||||
sortSeriesAscending,
|
||||
sortSeriesAscendingB,
|
||||
timeGrainSqla,
|
||||
percentageThreshold,
|
||||
metrics = [],
|
||||
@@ -222,14 +229,42 @@ export default function transformProps(
|
||||
}
|
||||
|
||||
const rebasedDataA = rebaseForecastDatum(data1, verboseMap);
|
||||
const [rawSeriesA] = extractSeries(rebasedDataA, {
|
||||
const { totalStackedValues, thresholdValues } = extractDataTotalValues(
|
||||
rebasedDataA,
|
||||
{
|
||||
stack,
|
||||
percentageThreshold,
|
||||
xAxisCol: xAxisLabel,
|
||||
},
|
||||
);
|
||||
|
||||
const MetricDisplayNameA = getMetricDisplayName(metrics[0], verboseMap);
|
||||
const MetricDisplayNameB = getMetricDisplayName(metricsB[0], verboseMap);
|
||||
|
||||
const [rawSeriesA, sortedTotalValuesA] = extractSeries(rebasedDataA, {
|
||||
fillNeighborValue: stack ? 0 : undefined,
|
||||
xAxis: xAxisLabel,
|
||||
sortSeriesType,
|
||||
sortSeriesAscending,
|
||||
stack,
|
||||
totalStackedValues,
|
||||
});
|
||||
const rebasedDataB = rebaseForecastDatum(data2, verboseMap);
|
||||
const [rawSeriesB] = extractSeries(rebasedDataB, {
|
||||
const {
|
||||
totalStackedValues: totalStackedValuesB,
|
||||
thresholdValues: thresholdValuesB,
|
||||
} = extractDataTotalValues(rebasedDataB, {
|
||||
stack: Boolean(stackB),
|
||||
percentageThreshold,
|
||||
xAxisCol: xAxisLabel,
|
||||
});
|
||||
const [rawSeriesB, sortedTotalValuesB] = extractSeries(rebasedDataB, {
|
||||
fillNeighborValue: stackB ? 0 : undefined,
|
||||
xAxis: xAxisLabel,
|
||||
sortSeriesType: sortSeriesTypeB,
|
||||
sortSeriesAscending: sortSeriesAscendingB,
|
||||
stack: Boolean(stackB),
|
||||
totalStackedValues: totalStackedValuesB,
|
||||
});
|
||||
|
||||
const dataTypes = getColtypesMapping(queriesData[0]);
|
||||
@@ -287,25 +322,11 @@ export default function transformProps(
|
||||
);
|
||||
const showValueIndexesA = extractShowValueIndexes(rawSeriesA, {
|
||||
stack,
|
||||
onlyTotal,
|
||||
});
|
||||
const showValueIndexesB = extractShowValueIndexes(rawSeriesB, {
|
||||
stack,
|
||||
});
|
||||
const { totalStackedValues, thresholdValues } = extractDataTotalValues(
|
||||
rebasedDataA,
|
||||
{
|
||||
stack,
|
||||
percentageThreshold,
|
||||
xAxisCol: xAxisLabel,
|
||||
},
|
||||
);
|
||||
const {
|
||||
totalStackedValues: totalStackedValuesB,
|
||||
thresholdValues: thresholdValuesB,
|
||||
} = extractDataTotalValues(rebasedDataB, {
|
||||
stack: Boolean(stackB),
|
||||
percentageThreshold,
|
||||
xAxisCol: xAxisLabel,
|
||||
onlyTotal,
|
||||
});
|
||||
|
||||
annotationLayers
|
||||
@@ -373,6 +394,12 @@ export default function transformProps(
|
||||
const seriesName = inverted[entryName] || entryName;
|
||||
const colorScaleKey = getOriginalSeries(seriesName, array);
|
||||
|
||||
let displayName = `${entryName} (Query A)`;
|
||||
|
||||
if (groupby.length > 0) {
|
||||
displayName = `${MetricDisplayNameA} (Query A), ${entryName}`;
|
||||
}
|
||||
|
||||
const seriesFormatter = getFormatter(
|
||||
customFormatters,
|
||||
formatter,
|
||||
@@ -382,7 +409,10 @@ export default function transformProps(
|
||||
);
|
||||
|
||||
const transformedSeries = transformSeries(
|
||||
entry,
|
||||
{
|
||||
...entry,
|
||||
id: `${displayName || ''}`,
|
||||
},
|
||||
colorScale,
|
||||
colorScaleKey,
|
||||
{
|
||||
@@ -392,6 +422,7 @@ export default function transformProps(
|
||||
areaOpacity: opacity,
|
||||
seriesType,
|
||||
showValue,
|
||||
onlyTotal,
|
||||
stack: Boolean(stack),
|
||||
stackIdSuffix: '\na',
|
||||
yAxisIndex,
|
||||
@@ -406,8 +437,8 @@ export default function transformProps(
|
||||
formatter: seriesFormatter,
|
||||
})
|
||||
: seriesFormatter,
|
||||
totalStackedValues: sortedTotalValuesA,
|
||||
showValueIndexes: showValueIndexesA,
|
||||
totalStackedValues,
|
||||
thresholdValues,
|
||||
timeShiftColor,
|
||||
},
|
||||
@@ -421,6 +452,12 @@ export default function transformProps(
|
||||
const seriesName = `${seriesEntry} (1)`;
|
||||
const colorScaleKey = getOriginalSeries(seriesEntry, array);
|
||||
|
||||
let displayName = `${entryName} (Query B)`;
|
||||
|
||||
if (groupbyB.length > 0) {
|
||||
displayName = `${MetricDisplayNameB} (Query B), ${entryName}`;
|
||||
}
|
||||
|
||||
const seriesFormatter = getFormatter(
|
||||
customFormattersSecondary,
|
||||
formatterSecondary,
|
||||
@@ -430,7 +467,11 @@ export default function transformProps(
|
||||
);
|
||||
|
||||
const transformedSeries = transformSeries(
|
||||
entry,
|
||||
{
|
||||
...entry,
|
||||
id: `${displayName || ''}`,
|
||||
},
|
||||
|
||||
colorScale,
|
||||
colorScaleKey,
|
||||
{
|
||||
@@ -440,13 +481,12 @@ export default function transformProps(
|
||||
areaOpacity: opacityB,
|
||||
seriesType: seriesTypeB,
|
||||
showValue: showValueB,
|
||||
onlyTotal: onlyTotalB,
|
||||
stack: Boolean(stackB),
|
||||
stackIdSuffix: '\nb',
|
||||
yAxisIndex: yAxisIndexB,
|
||||
filterState,
|
||||
seriesKey: primarySeries.has(entry.name as string)
|
||||
? `${entry.name} (1)`
|
||||
: entry.name,
|
||||
seriesKey: entry.name,
|
||||
sliceId,
|
||||
queryIndex: 1,
|
||||
formatter:
|
||||
@@ -456,8 +496,8 @@ export default function transformProps(
|
||||
formatter: seriesFormatter,
|
||||
})
|
||||
: seriesFormatter,
|
||||
totalStackedValues: sortedTotalValuesB,
|
||||
showValueIndexes: showValueIndexesB,
|
||||
totalStackedValues: totalStackedValuesB,
|
||||
thresholdValues: thresholdValuesB,
|
||||
timeShiftColor,
|
||||
},
|
||||
|
||||
@@ -16,14 +16,30 @@
|
||||
* specific language governing permissions and limitations
|
||||
* 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) {
|
||||
const { metric, sort_by_metric } = formData;
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
...(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',
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ValueFormatter,
|
||||
getValueFormatter,
|
||||
tooltipHtml,
|
||||
DataRecord,
|
||||
} from '@superset-ui/core';
|
||||
import type { CallbackDataParams } from 'echarts/types/src/util/types';
|
||||
import type { EChartsCoreOption } from 'echarts/core';
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
EchartsPieChartProps,
|
||||
EchartsPieFormData,
|
||||
EchartsPieLabelType,
|
||||
PieChartDataItem,
|
||||
PieChartTransformedProps,
|
||||
} from './types';
|
||||
import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants';
|
||||
@@ -50,6 +52,7 @@ import { defaultGrid } from '../defaults';
|
||||
import { convertInteger } from '../utils/convertInteger';
|
||||
import { getDefaultTooltip } from '../utils/tooltip';
|
||||
import { Refs } from '../types';
|
||||
import { getContributionLabel } from './utils';
|
||||
|
||||
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
|
||||
|
||||
@@ -133,7 +136,7 @@ export default function transformProps(
|
||||
datasource,
|
||||
} = chartProps;
|
||||
const { columnFormats = {}, currencyFormats = {} } = datasource;
|
||||
const { data = [] } = queriesData[0];
|
||||
const { data: rawData = [] } = queriesData[0];
|
||||
const coltypeMapping = getColtypesMapping(queriesData[0]);
|
||||
|
||||
const {
|
||||
@@ -159,6 +162,7 @@ export default function transformProps(
|
||||
sliceId,
|
||||
showTotal,
|
||||
roseType,
|
||||
thresholdForOther,
|
||||
}: EchartsPieFormData = {
|
||||
...DEFAULT_LEGEND_FORM_DATA,
|
||||
...DEFAULT_PIE_FORM_DATA,
|
||||
@@ -166,17 +170,68 @@ export default function transformProps(
|
||||
};
|
||||
const refs: Refs = {};
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
const contributionLabel = getContributionLabel(metricLabel);
|
||||
const groupbyLabels = groupby.map(getColumnLabel);
|
||||
const minShowLabelAngle = (showLabelsThreshold || 0) * 3.6;
|
||||
|
||||
const keys = data.map(datum =>
|
||||
extractGroupbyLabel({
|
||||
datum,
|
||||
groupby: groupbyLabels,
|
||||
coltypeMapping,
|
||||
timeFormatter: getTimeFormatter(dateFormat),
|
||||
}),
|
||||
const numberFormatter = getValueFormatter(
|
||||
metric,
|
||||
currencyFormats,
|
||||
columnFormats,
|
||||
numberFormat,
|
||||
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 label = extractGroupbyLabel({
|
||||
datum,
|
||||
@@ -192,13 +247,6 @@ export default function transformProps(
|
||||
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getValueFormatter(
|
||||
metric,
|
||||
currencyFormats,
|
||||
columnFormats,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
);
|
||||
|
||||
let totalValue = 0;
|
||||
|
||||
@@ -229,6 +277,10 @@ export default function transformProps(
|
||||
},
|
||||
};
|
||||
});
|
||||
if (otherDatum) {
|
||||
transformedData.push(otherDatum);
|
||||
totalValue += otherSum;
|
||||
}
|
||||
|
||||
const selectedValues = (filterState.selectedValues || []).reduce(
|
||||
(acc: Record<string, number>, selectedValue: string) => {
|
||||
@@ -372,6 +424,9 @@ export default function transformProps(
|
||||
numberFormatter,
|
||||
sanitizeName: true,
|
||||
});
|
||||
if (params?.data?.isOther) {
|
||||
return tooltipHtml(otherTooltipData, name);
|
||||
}
|
||||
return tooltipHtml(
|
||||
[[metricLabel, formattedValue, formattedPercent]],
|
||||
name,
|
||||
@@ -380,7 +435,7 @@ export default function transformProps(
|
||||
},
|
||||
legend: {
|
||||
...getLegendProps(legendType, legendOrientation, showLegend, theme),
|
||||
data: keys,
|
||||
data: transformedData.map(datum => datum.name),
|
||||
},
|
||||
graphic: showTotal
|
||||
? {
|
||||
|
||||
@@ -47,6 +47,7 @@ export type EchartsPieFormData = QueryFormData &
|
||||
dateFormat: string;
|
||||
showLabelsThreshold: number;
|
||||
roseType: 'radius' | 'area' | null;
|
||||
thresholdForOther: number;
|
||||
};
|
||||
|
||||
export enum EchartsPieLabelType {
|
||||
@@ -82,9 +83,20 @@ export const DEFAULT_FORM_DATA: EchartsPieFormData = {
|
||||
showLabelsThreshold: 5,
|
||||
dateFormat: 'smart_date',
|
||||
roseType: null,
|
||||
thresholdForOther: 0,
|
||||
};
|
||||
|
||||
export type PieChartTransformedProps =
|
||||
BaseTransformedProps<EchartsPieFormData> &
|
||||
ContextMenuTransformedProps &
|
||||
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}`;
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
getNumberFormatter,
|
||||
getTimeFormatter,
|
||||
NumberFormatter,
|
||||
isDefined,
|
||||
} from '@superset-ui/core';
|
||||
import type { CallbackDataParams } from 'echarts/types/src/util/types';
|
||||
import type { RadarSeriesDataItemOption } from 'echarts/types/src/chart/radar/RadarSeries';
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
EchartsRadarFormData,
|
||||
EchartsRadarLabelType,
|
||||
RadarChartTransformedProps,
|
||||
SeriesNormalizedMap,
|
||||
} from './types';
|
||||
import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants';
|
||||
import {
|
||||
@@ -46,18 +48,31 @@ import {
|
||||
import { defaultGrid } from '../defaults';
|
||||
import { Refs } from '../types';
|
||||
import { getDefaultTooltip } from '../utils/tooltip';
|
||||
import { findGlobalMax, renderNormalizedTooltip } from './utils';
|
||||
|
||||
export function formatLabel({
|
||||
params,
|
||||
labelType,
|
||||
numberFormatter,
|
||||
getDenormalizedSeriesValue,
|
||||
metricsWithCustomBounds,
|
||||
metricLabels,
|
||||
}: {
|
||||
params: CallbackDataParams;
|
||||
labelType: EchartsRadarLabelType;
|
||||
numberFormatter: NumberFormatter;
|
||||
getDenormalizedSeriesValue: (seriesName: string, value: string) => number;
|
||||
metricsWithCustomBounds: Set<string>;
|
||||
metricLabels: string[];
|
||||
}): string {
|
||||
const { name = '', value } = params;
|
||||
const formattedValue = numberFormatter(value as number);
|
||||
const { name = '', value, dimensionIndex = 0 } = params;
|
||||
const metricLabel = metricLabels[dimensionIndex];
|
||||
|
||||
const formattedValue = numberFormatter(
|
||||
metricsWithCustomBounds.has(metricLabel)
|
||||
? (value as number)
|
||||
: (getDenormalizedSeriesValue(name, String(value)) as number),
|
||||
);
|
||||
|
||||
switch (labelType) {
|
||||
case EchartsRadarLabelType.Value:
|
||||
@@ -85,6 +100,7 @@ export default function transformProps(
|
||||
} = chartProps;
|
||||
const refs: Refs = {};
|
||||
const { data = [] } = queriesData[0];
|
||||
const globalMax = findGlobalMax(data, Object.keys(data[0] || {}));
|
||||
const coltypeMapping = getColtypesMapping(queriesData[0]);
|
||||
|
||||
const {
|
||||
@@ -111,14 +127,38 @@ export default function transformProps(
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getNumberFormatter(numberFormat);
|
||||
const denormalizedSeriesValues: SeriesNormalizedMap = {};
|
||||
|
||||
const getDenormalizedSeriesValue = (
|
||||
seriesName: string,
|
||||
normalizedValue: string,
|
||||
): number =>
|
||||
denormalizedSeriesValues?.[seriesName]?.[normalizedValue] ??
|
||||
Number(normalizedValue);
|
||||
|
||||
const metricLabels = metrics.map(getMetricLabel);
|
||||
|
||||
const metricsWithCustomBounds = new Set(
|
||||
metricLabels.filter(metricLabel => {
|
||||
const config = columnConfig?.[metricLabel];
|
||||
const hasMax = !!isDefined(config?.radarMetricMaxValue);
|
||||
const hasMin =
|
||||
isDefined(config?.radarMetricMinValue) &&
|
||||
config?.radarMetricMinValue !== 0;
|
||||
return hasMax || hasMin;
|
||||
}),
|
||||
);
|
||||
|
||||
const formatter = (params: CallbackDataParams) =>
|
||||
formatLabel({
|
||||
params,
|
||||
numberFormatter,
|
||||
labelType,
|
||||
getDenormalizedSeriesValue,
|
||||
metricsWithCustomBounds,
|
||||
metricLabels,
|
||||
});
|
||||
|
||||
const metricLabels = metrics.map(getMetricLabel);
|
||||
const groupbyLabels = groupby.map(getColumnLabel);
|
||||
|
||||
const metricLabelAndMaxValueMap = new Map<string, number>();
|
||||
@@ -212,28 +252,58 @@ export default function transformProps(
|
||||
{},
|
||||
);
|
||||
|
||||
const normalizeArray = (arr: number[], decimals = 10, seriesName: string) =>
|
||||
arr.map((value, index) => {
|
||||
const metricLabel = metricLabels[index];
|
||||
if (metricsWithCustomBounds.has(metricLabel)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const max = Math.max(...arr);
|
||||
const normalizedValue = Number((value / max).toFixed(decimals));
|
||||
|
||||
denormalizedSeriesValues[seriesName][String(normalizedValue)] = value;
|
||||
return normalizedValue;
|
||||
});
|
||||
|
||||
// Normalize the transformed data
|
||||
const normalizedTransformedData = transformedData.map(series => {
|
||||
if (Array.isArray(series.value)) {
|
||||
const seriesName = String(series?.name || '');
|
||||
denormalizedSeriesValues[seriesName] = {};
|
||||
|
||||
return {
|
||||
...series,
|
||||
value: normalizeArray(series.value as number[], 10, seriesName),
|
||||
};
|
||||
}
|
||||
return series;
|
||||
});
|
||||
|
||||
const indicator = metricLabels.map(metricLabel => {
|
||||
const isMetricWithCustomBounds = metricsWithCustomBounds.has(metricLabel);
|
||||
if (!isMetricWithCustomBounds) {
|
||||
return {
|
||||
name: metricLabel,
|
||||
max: 1,
|
||||
min: 0,
|
||||
};
|
||||
}
|
||||
const maxValueInControl = columnConfig?.[metricLabel]?.radarMetricMaxValue;
|
||||
const minValueInControl = columnConfig?.[metricLabel]?.radarMetricMinValue;
|
||||
|
||||
// Ensure that 0 is at the center of the polar coordinates
|
||||
const metricValueAsMax =
|
||||
const maxValue =
|
||||
metricLabelAndMaxValueMap.get(metricLabel) === 0
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: metricLabelAndMaxValueMap.get(metricLabel);
|
||||
const max =
|
||||
maxValueInControl === null ? metricValueAsMax : maxValueInControl;
|
||||
: globalMax;
|
||||
const max = isDefined(maxValueInControl) ? maxValueInControl : maxValue;
|
||||
|
||||
let min: number;
|
||||
// If the min value doesn't exist, set it to 0 (default),
|
||||
// if it is null, set it to the min value of the data,
|
||||
// otherwise, use the value from the control
|
||||
if (minValueInControl === undefined) {
|
||||
min = 0;
|
||||
} else if (minValueInControl === null) {
|
||||
min = metricLabelAndMinValueMap.get(metricLabel) || 0;
|
||||
} else {
|
||||
if (isDefined(minValueInControl)) {
|
||||
min = minValueInControl;
|
||||
} else {
|
||||
min = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -255,10 +325,24 @@ export default function transformProps(
|
||||
backgroundColor: theme.colors.grayscale.light5,
|
||||
},
|
||||
},
|
||||
data: transformedData,
|
||||
data: normalizedTransformedData,
|
||||
},
|
||||
];
|
||||
|
||||
const NormalizedTooltipFormater = (
|
||||
params: CallbackDataParams & {
|
||||
color: string;
|
||||
name: string;
|
||||
value: number[];
|
||||
},
|
||||
) =>
|
||||
renderNormalizedTooltip(
|
||||
params,
|
||||
metricLabels,
|
||||
getDenormalizedSeriesValue,
|
||||
metricsWithCustomBounds,
|
||||
);
|
||||
|
||||
const echartOptions: EChartsCoreOption = {
|
||||
grid: {
|
||||
...defaultGrid,
|
||||
@@ -267,6 +351,7 @@ export default function transformProps(
|
||||
...getDefaultTooltip(refs),
|
||||
show: !inContextMenu,
|
||||
trigger: 'item',
|
||||
formatter: NormalizedTooltipFormater,
|
||||
},
|
||||
legend: {
|
||||
...getLegendProps(legendType, legendOrientation, showLegend, theme),
|
||||
|
||||
@@ -35,7 +35,7 @@ import { DEFAULT_LEGEND_FORM_DATA } from '../constants';
|
||||
|
||||
type RadarColumnConfig = Record<
|
||||
string,
|
||||
{ radarMetricMaxValue?: number; radarMetricMinValue?: number }
|
||||
{ radarMetricMaxValue?: number | null; radarMetricMinValue?: number }
|
||||
>;
|
||||
|
||||
export type EchartsRadarFormData = QueryFormData &
|
||||
@@ -53,6 +53,7 @@ export type EchartsRadarFormData = QueryFormData &
|
||||
isCircle: boolean;
|
||||
numberFormat: string;
|
||||
dateFormat: string;
|
||||
isNormalized: boolean;
|
||||
};
|
||||
|
||||
export enum EchartsRadarLabelType {
|
||||
@@ -83,3 +84,17 @@ export type RadarChartTransformedProps =
|
||||
BaseTransformedProps<EchartsRadarFormData> &
|
||||
ContextMenuTransformedProps &
|
||||
CrossFilterTransformedProps;
|
||||
|
||||
/**
|
||||
* Represents a mapping from a normalized value (as string) to an original numeric value.
|
||||
*/
|
||||
interface NormalizedValueMap {
|
||||
[normalized: string]: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a collection of series, each containing its own NormalizedValueMap.
|
||||
*/
|
||||
export interface SeriesNormalizedMap {
|
||||
[seriesName: string]: NormalizedValueMap;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
function for finding the max metric values among all series data for Radar Chart
|
||||
*/
|
||||
export const findGlobalMax = (
|
||||
data: Record<string, unknown>[],
|
||||
metrics: string[],
|
||||
): number => {
|
||||
if (!data?.length || !metrics?.length) return 0;
|
||||
|
||||
return data.reduce((globalMax, row) => {
|
||||
const rowMax = metrics.reduce((max, metric) => {
|
||||
const value = row[metric];
|
||||
return typeof value === 'number' &&
|
||||
Number.isFinite(value) &&
|
||||
!Number.isNaN(value)
|
||||
? Math.max(max, value)
|
||||
: max;
|
||||
}, 0);
|
||||
|
||||
return Math.max(globalMax, rowMax);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
interface TooltipParams {
|
||||
color: string;
|
||||
name?: string;
|
||||
value: number[];
|
||||
}
|
||||
|
||||
interface TooltipMetricValue {
|
||||
metric: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const renderNormalizedTooltip = (
|
||||
params: TooltipParams,
|
||||
metrics: string[],
|
||||
getDenormalizedValue: (seriesName: string, value: string) => number,
|
||||
metricsWithCustomBounds: Set<string>,
|
||||
): string => {
|
||||
const { color, name = '', value: values } = params;
|
||||
const seriesName = name || 'series0';
|
||||
|
||||
const colorDot = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:5px;height:5px;background-color:${color}"></span>`;
|
||||
|
||||
// Get metric values with denormalization if needed
|
||||
const metricValues: TooltipMetricValue[] = metrics.map((metric, index) => {
|
||||
const value = values[index];
|
||||
const originalValue = metricsWithCustomBounds.has(metric)
|
||||
? value
|
||||
: getDenormalizedValue(name, String(value));
|
||||
|
||||
return {
|
||||
metric,
|
||||
value: originalValue,
|
||||
};
|
||||
});
|
||||
|
||||
const tooltipRows = metricValues
|
||||
.map(
|
||||
({ metric, value }) => `
|
||||
<div style="display:flex;">
|
||||
<div>${colorDot}${metric}:</div>
|
||||
<div style="font-weight:bold;margin-left:auto;">${value}</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<div style="font-weight:bold;margin-bottom:5px;">${seriesName}</div>
|
||||
${tooltipRows}
|
||||
`;
|
||||
};
|
||||
@@ -57,7 +57,7 @@ export default class EchartsSankeyChartPlugin extends ChartPlugin<
|
||||
),
|
||||
exampleGallery: [{ url: example1 }, { url: example2 }],
|
||||
name: t('Sankey Chart'),
|
||||
tags: [t('Directional'), t('Distribution'), t('Flow')],
|
||||
tags: [t('Directional'), t('ECharts'), t('Distribution'), t('Flow')],
|
||||
thumbnail,
|
||||
}),
|
||||
transformProps,
|
||||
|
||||
@@ -73,13 +73,25 @@ export default function transformProps(
|
||||
}));
|
||||
|
||||
// stores a map with the total values for each node considering the links
|
||||
const nodeValues = new Map<string, number>();
|
||||
const incomingFlows = new Map<string, number>();
|
||||
const outgoingFlows = new Map<string, number>();
|
||||
const allNodeNames = new Set<string>();
|
||||
|
||||
links.forEach(link => {
|
||||
const { source, target, value } = link;
|
||||
const sourceValue = nodeValues.get(source) || 0;
|
||||
const targetValue = nodeValues.get(target) || 0;
|
||||
nodeValues.set(source, sourceValue + value);
|
||||
nodeValues.set(target, targetValue + value);
|
||||
allNodeNames.add(source);
|
||||
allNodeNames.add(target);
|
||||
incomingFlows.set(target, (incomingFlows.get(target) || 0) + value);
|
||||
outgoingFlows.set(source, (outgoingFlows.get(source) || 0) + value);
|
||||
});
|
||||
|
||||
const nodeValues = new Map<string, number>();
|
||||
|
||||
allNodeNames.forEach(nodeName => {
|
||||
const totalIncoming = incomingFlows.get(nodeName) || 0;
|
||||
const totalOutgoing = outgoingFlows.get(nodeName) || 0;
|
||||
|
||||
nodeValues.set(nodeName, Math.max(totalIncoming, totalOutgoing));
|
||||
});
|
||||
|
||||
const tooltipFormatter = (params: CallbackDataParams) => {
|
||||
|
||||
@@ -65,6 +65,8 @@ export default function EchartsTimeseries({
|
||||
const clickTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
const extraControlRef = useRef<HTMLDivElement>(null);
|
||||
const [extraControlHeight, setExtraControlHeight] = useState(0);
|
||||
// Track the current legend page index
|
||||
const [currentLegendPage, setCurrentLegendPage] = useState(0);
|
||||
useEffect(() => {
|
||||
const updatedHeight = extraControlRef.current?.offsetHeight || 0;
|
||||
setExtraControlHeight(updatedHeight);
|
||||
@@ -139,6 +141,20 @@ export default function EchartsTimeseries({
|
||||
[emitCrossFilters, setDataMask, getCrossFilterDataMask],
|
||||
);
|
||||
|
||||
// Helper function to restore legend page position
|
||||
const restoreLegendPagePosition = useCallback(() => {
|
||||
// Use requestAnimationFrame instead of setTimeout for better alignment with the browser's rendering cycle
|
||||
requestAnimationFrame(() => {
|
||||
const echartInstance = echartRef.current?.getEchartInstance();
|
||||
if (echartInstance) {
|
||||
echartInstance.dispatchAction({
|
||||
type: 'legendScroll',
|
||||
scrollDataIndex: currentLegendPage,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [currentLegendPage]);
|
||||
|
||||
const eventHandlers: EventHandlers = {
|
||||
click: props => {
|
||||
if (!hasDimensions) {
|
||||
@@ -161,12 +177,21 @@ export default function EchartsTimeseries({
|
||||
},
|
||||
legendselectchanged: payload => {
|
||||
onLegendStateChanged?.(payload.selected);
|
||||
restoreLegendPagePosition();
|
||||
},
|
||||
legendselectall: payload => {
|
||||
onLegendStateChanged?.(payload.selected);
|
||||
restoreLegendPagePosition();
|
||||
},
|
||||
legendinverseselect: payload => {
|
||||
onLegendStateChanged?.(payload.selected);
|
||||
restoreLegendPagePosition();
|
||||
},
|
||||
// Add handler for legend scroll event to track current page
|
||||
legendscroll: params => {
|
||||
if (params.scrollDataIndex !== undefined) {
|
||||
setCurrentLegendPage(params.scrollDataIndex);
|
||||
}
|
||||
},
|
||||
contextmenu: async eventParams => {
|
||||
if (onContextMenu) {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
import { JsonArray, t } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
ControlPanelsContainerProps,
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
DEFAULT_FORM_DATA,
|
||||
TIME_SERIES_DESCRIPTION_TEXT,
|
||||
} from '../../constants';
|
||||
import { StackControlsValue } from '../../../constants';
|
||||
|
||||
const {
|
||||
logAxis,
|
||||
@@ -321,6 +322,38 @@ const config: ControlPanelConfig = {
|
||||
['color_scheme'],
|
||||
['time_shift_color'],
|
||||
...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],
|
||||
[
|
||||
{
|
||||
|
||||
@@ -58,6 +58,7 @@ export default class EchartsTimeseriesChartPlugin extends EchartsChartPlugin<
|
||||
name: t('Generic Chart'),
|
||||
tags: [
|
||||
t('Advanced-Analytics'),
|
||||
t('ECharts'),
|
||||
t('Line'),
|
||||
t('Predictive'),
|
||||
t('Time'),
|
||||
|
||||
@@ -191,6 +191,7 @@ export default function transformProps(
|
||||
yAxisTitleMargin,
|
||||
yAxisTitlePosition,
|
||||
zoomable,
|
||||
stackDimension,
|
||||
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
||||
const refs: Refs = {};
|
||||
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
|
||||
const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound);
|
||||
let [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound);
|
||||
|
||||
@@ -74,6 +74,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
|
||||
rowLimit: number;
|
||||
seriesType: EchartsTimeseriesSeriesType;
|
||||
stack: StackType;
|
||||
stackDimension: string;
|
||||
timeCompare?: string[];
|
||||
tooltipTimeFormat?: string;
|
||||
showTooltipTotal?: boolean;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 { QueryFormMetric } from '@superset-ui/core';
|
||||
|
||||
export const getMetricDisplayName = (
|
||||
metric: QueryFormMetric,
|
||||
verboseMap: Record<string, string> = {},
|
||||
): string => {
|
||||
// Case 1: Simple string metric - use verboseMap or the string itself
|
||||
if (typeof metric === 'string') {
|
||||
return verboseMap[metric] || metric;
|
||||
}
|
||||
|
||||
// Case 2: Metric with explicit label - always prefer this if available
|
||||
if (metric.label) {
|
||||
return metric.label;
|
||||
}
|
||||
|
||||
// Case 3: SIMPLE expression type (column with aggregate)
|
||||
if (metric.expressionType === 'SIMPLE') {
|
||||
const column = metric.column || {};
|
||||
const columnName = column.column_name || '';
|
||||
// Use verbose name from column if available
|
||||
const displayName = column.verbose_name || columnName;
|
||||
const aggregate = metric.aggregate || '';
|
||||
|
||||
// If the verbose map has this column, use that
|
||||
if (verboseMap[columnName]) {
|
||||
return `${aggregate}(${verboseMap[columnName]})`;
|
||||
}
|
||||
|
||||
return `${aggregate}(${displayName})`;
|
||||
}
|
||||
|
||||
// Case 4: SQL expression
|
||||
if (metric.expressionType === 'SQL') {
|
||||
return metric.sqlExpression || 'Custom SQL Metric';
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return 'Unknown Metric';
|
||||
};
|
||||
@@ -118,7 +118,9 @@ const chartPropsConfig = {
|
||||
|
||||
it('should transform chart props for viz', () => {
|
||||
const chartProps = new ChartProps(chartPropsConfig);
|
||||
expect(transformProps(chartProps as EchartsMixedTimeseriesProps)).toEqual(
|
||||
const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps);
|
||||
|
||||
expect(transformed).toEqual(
|
||||
expect.objectContaining({
|
||||
echartOptions: expect.objectContaining({
|
||||
series: expect.arrayContaining([
|
||||
@@ -127,7 +129,7 @@ it('should transform chart props for viz', () => {
|
||||
[599616000000, 1],
|
||||
[599916000000, 3],
|
||||
],
|
||||
id: 'boy',
|
||||
id: 'sum__num (Query A), boy',
|
||||
stack: 'obs\na',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@@ -135,15 +137,16 @@ it('should transform chart props for viz', () => {
|
||||
[599616000000, 2],
|
||||
[599916000000, 4],
|
||||
],
|
||||
id: 'girl',
|
||||
id: 'sum__num (Query A), girl',
|
||||
stack: 'obs\na',
|
||||
}),
|
||||
// Query B — Bar series
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 1],
|
||||
[599916000000, 3],
|
||||
],
|
||||
id: 'boy (1)',
|
||||
id: 'sum__num (Query B), boy',
|
||||
stack: 'obs\nb',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@@ -151,7 +154,7 @@ it('should transform chart props for viz', () => {
|
||||
[599616000000, 2],
|
||||
[599916000000, 4],
|
||||
],
|
||||
id: 'girl (1)',
|
||||
id: 'sum__num (Query B), girl',
|
||||
stack: 'obs\nb',
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
CallbackDataParams,
|
||||
} from 'echarts/types/src/util/types';
|
||||
import transformProps, { parseParams } from '../../src/Pie/transformProps';
|
||||
import { EchartsPieChartProps } from '../../src/Pie/types';
|
||||
import { EchartsPieChartProps, PieChartDataItem } from '../../src/Pie/types';
|
||||
|
||||
describe('Pie transformProps', () => {
|
||||
const formData: SqlaFormData = {
|
||||
@@ -46,8 +46,13 @@ describe('Pie transformProps', () => {
|
||||
queriesData: [
|
||||
{
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 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 { ChartProps, supersetTheme } from '@superset-ui/core';
|
||||
import { RadarSeriesOption } from 'echarts/charts';
|
||||
import transformProps from '../../src/Radar/transformProps';
|
||||
import {
|
||||
EchartsRadarChartProps,
|
||||
EchartsRadarFormData,
|
||||
} from '../../src/Radar/types';
|
||||
|
||||
interface RadarIndicator {
|
||||
name: string;
|
||||
max: number;
|
||||
min: number;
|
||||
}
|
||||
|
||||
type RadarShape = 'circle' | 'polygon';
|
||||
|
||||
interface RadarChartConfig {
|
||||
shape: RadarShape;
|
||||
indicator: RadarIndicator[];
|
||||
}
|
||||
|
||||
interface RadarSeriesData {
|
||||
value: number[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
describe('Radar transformProps', () => {
|
||||
const formData: Partial<EchartsRadarFormData> = {
|
||||
colorScheme: 'supersetColors',
|
||||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
columnConfig: {
|
||||
'MAX(na_sales)': {
|
||||
radarMetricMaxValue: null,
|
||||
radarMetricMinValue: 0,
|
||||
},
|
||||
'SUM(eu_sales)': {
|
||||
radarMetricMaxValue: 5000,
|
||||
},
|
||||
},
|
||||
groupby: [],
|
||||
metrics: [
|
||||
'MAX(na_sales)',
|
||||
'SUM(jp_sales)',
|
||||
'SUM(other_sales)',
|
||||
'SUM(eu_sales)',
|
||||
],
|
||||
viz_type: 'radar',
|
||||
numberFormat: 'SMART_NUMBER',
|
||||
dateFormat: 'smart_date',
|
||||
showLegend: true,
|
||||
showLabels: true,
|
||||
isCircle: false,
|
||||
};
|
||||
|
||||
const chartProps = new ChartProps({
|
||||
formData,
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
'MAX(na_sales)': 41.49,
|
||||
'SUM(jp_sales)': 1290.99,
|
||||
'SUM(other_sales)': 797.73,
|
||||
'SUM(eu_sales)': 2434.13,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
theme: supersetTheme,
|
||||
});
|
||||
|
||||
it('should transform chart props for normalized radar chart & normalize all metrics except the ones with custom min & max', () => {
|
||||
const transformedProps = transformProps(
|
||||
chartProps as EchartsRadarChartProps,
|
||||
);
|
||||
const series = transformedProps.echartOptions.series as RadarSeriesOption[];
|
||||
const radar = transformedProps.echartOptions.radar as RadarChartConfig;
|
||||
|
||||
expect((series[0].data as RadarSeriesData[])[0].value).toEqual([
|
||||
0.0170451044, 0.5303701939, 0.3277269497, 2434.13,
|
||||
]);
|
||||
|
||||
expect(radar.indicator).toEqual([
|
||||
{
|
||||
name: 'MAX(na_sales)',
|
||||
max: 1,
|
||||
min: 0,
|
||||
},
|
||||
{
|
||||
name: 'SUM(jp_sales)',
|
||||
max: 1,
|
||||
min: 0,
|
||||
},
|
||||
{
|
||||
name: 'SUM(other_sales)',
|
||||
max: 1,
|
||||
min: 0,
|
||||
},
|
||||
{
|
||||
name: 'SUM(eu_sales)',
|
||||
max: 5000,
|
||||
min: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
MutableRefObject,
|
||||
CSSProperties,
|
||||
DragEvent,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
@@ -39,8 +41,9 @@ import {
|
||||
Row,
|
||||
} from 'react-table';
|
||||
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 { Space } from 'antd';
|
||||
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
|
||||
import SelectPageSize, {
|
||||
SelectPageSizeProps,
|
||||
@@ -50,6 +53,8 @@ import SimplePagination from './components/Pagination';
|
||||
import useSticky from './hooks/useSticky';
|
||||
import { PAGE_SIZE_OPTIONS } from '../consts';
|
||||
import { sortAlphanumericCaseInsensitive } from './utils/sortAlphanumericCaseInsensitive';
|
||||
import { SearchOption, SortByItem } from '../types';
|
||||
import SearchSelectDropdown from './components/SearchSelectDropdown';
|
||||
|
||||
export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
tableClassName?: string;
|
||||
@@ -62,7 +67,12 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
height?: string | number;
|
||||
serverPagination?: boolean;
|
||||
onServerPaginationChange: (pageNumber: number, pageSize: number) => void;
|
||||
serverPaginationData: { pageSize?: number; currentPage?: number };
|
||||
serverPaginationData: {
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
sortBy?: SortByItem[];
|
||||
searchColumn?: string;
|
||||
};
|
||||
pageSize?: number;
|
||||
noResults?: string | ((filterString: string) => ReactNode);
|
||||
sticky?: boolean;
|
||||
@@ -71,6 +81,14 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
onColumnOrderChange: () => void;
|
||||
renderGroupingHeaders?: () => JSX.Element;
|
||||
renderTimeComparisonDropdown?: () => JSX.Element;
|
||||
handleSortByChange: (sortBy: SortByItem[]) => void;
|
||||
sortByFromParent: SortByItem[];
|
||||
manualSearch?: boolean;
|
||||
onSearchChange?: (searchText: string) => void;
|
||||
initialSearchText?: string;
|
||||
searchInputId?: string;
|
||||
onSearchColChange: (searchCol: string) => void;
|
||||
searchOptions: SearchOption[];
|
||||
}
|
||||
|
||||
export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
|
||||
@@ -81,6 +99,24 @@ const sortTypes = {
|
||||
alphanumeric: sortAlphanumericCaseInsensitive,
|
||||
};
|
||||
|
||||
const StyledSpace = styled(Space)`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.search-select-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-by-label {
|
||||
align-self: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRow = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
// Be sure to pass our updateMyData and the skipReset option
|
||||
export default typedMemo(function DataTable<D extends object>({
|
||||
tableClassName,
|
||||
@@ -105,6 +141,14 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
onColumnOrderChange,
|
||||
renderGroupingHeaders,
|
||||
renderTimeComparisonDropdown,
|
||||
handleSortByChange,
|
||||
sortByFromParent = [],
|
||||
manualSearch = false,
|
||||
onSearchChange,
|
||||
initialSearchText,
|
||||
searchInputId,
|
||||
onSearchColChange,
|
||||
searchOptions,
|
||||
...moreUseTableOptions
|
||||
}: DataTableProps<D>): JSX.Element {
|
||||
const tableHooks: PluginHook<D>[] = [
|
||||
@@ -115,6 +159,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
doSticky ? useSticky : [],
|
||||
hooks || [],
|
||||
].flat();
|
||||
|
||||
const columnNames = Object.keys(data?.[0] || {});
|
||||
const previousColumnNames = usePrevious(columnNames);
|
||||
const resultsSize = serverPagination ? rowCount : data.length;
|
||||
@@ -127,7 +172,8 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
...initialState_,
|
||||
// zero length means all pages, the `usePagination` plugin does not
|
||||
// understand pageSize = 0
|
||||
sortBy: sortByRef.current,
|
||||
// sortBy: sortByRef.current,
|
||||
sortBy: serverPagination ? sortByFromParent : sortByRef.current,
|
||||
pageSize: initialPageSize > 0 ? initialPageSize : resultsSize || 10,
|
||||
};
|
||||
const defaultWrapperRef = useRef<HTMLDivElement>(null);
|
||||
@@ -188,7 +234,13 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
wrapStickyTable,
|
||||
setColumnOrder,
|
||||
allColumns,
|
||||
state: { pageIndex, pageSize, globalFilter: filterValue, sticky = {} },
|
||||
state: {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
globalFilter: filterValue,
|
||||
sticky = {},
|
||||
sortBy,
|
||||
},
|
||||
} = useTable<D>(
|
||||
{
|
||||
columns,
|
||||
@@ -198,10 +250,46 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
globalFilter: defaultGlobalFilter,
|
||||
sortTypes,
|
||||
autoResetSortBy: !isEqual(columnNames, previousColumnNames),
|
||||
manualSortBy: !!serverPagination,
|
||||
...moreUseTableOptions,
|
||||
},
|
||||
...tableHooks,
|
||||
);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(query: string) => {
|
||||
if (manualSearch && onSearchChange) {
|
||||
onSearchChange(query);
|
||||
} else {
|
||||
setGlobalFilter(query);
|
||||
}
|
||||
},
|
||||
[manualSearch, onSearchChange, setGlobalFilter],
|
||||
);
|
||||
|
||||
// updating the sort by to the own State of table viz
|
||||
useEffect(() => {
|
||||
const serverSortBy = serverPaginationData?.sortBy || [];
|
||||
|
||||
if (serverPagination && !isEqual(sortBy, serverSortBy)) {
|
||||
if (Array.isArray(sortBy) && sortBy.length > 0) {
|
||||
const [sortByItem] = sortBy;
|
||||
const matchingColumn = columns.find(col => col?.id === sortByItem?.id);
|
||||
|
||||
if (matchingColumn && 'columnKey' in matchingColumn) {
|
||||
const sortByWithColumnKey: SortByItem = {
|
||||
...sortByItem,
|
||||
key: (matchingColumn as { columnKey: string }).columnKey,
|
||||
};
|
||||
|
||||
handleSortByChange([sortByWithColumnKey]);
|
||||
}
|
||||
} else {
|
||||
handleSortByChange([]);
|
||||
}
|
||||
}
|
||||
}, [sortBy]);
|
||||
|
||||
// make setPageSize accept 0
|
||||
const setPageSize = (size: number) => {
|
||||
if (serverPagination) {
|
||||
@@ -355,6 +443,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
resultOnPageChange = (pageNumber: number) =>
|
||||
onServerPaginationChange(pageNumber, serverPageSize);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
@@ -362,9 +451,9 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
>
|
||||
{hasGlobalControl ? (
|
||||
<div ref={globalControlRef} className="form-inline dt-controls">
|
||||
<div className="row">
|
||||
<StyledRow className="row">
|
||||
<div
|
||||
className={renderTimeComparisonDropdown ? 'col-sm-5' : 'col-sm-6'}
|
||||
className={renderTimeComparisonDropdown ? 'col-sm-4' : 'col-sm-5'}
|
||||
>
|
||||
{hasPagination ? (
|
||||
<SelectPageSize
|
||||
@@ -381,16 +470,35 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
) : null}
|
||||
</div>
|
||||
{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>
|
||||
searchInput={
|
||||
typeof searchInput === 'boolean' ? undefined : searchInput
|
||||
}
|
||||
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||
setGlobalFilter={setGlobalFilter}
|
||||
filterValue={filterValue}
|
||||
setGlobalFilter={
|
||||
manualSearch ? handleSearchChange : setGlobalFilter
|
||||
}
|
||||
filterValue={manualSearch ? initialSearchText : filterValue}
|
||||
id={searchInputId}
|
||||
serverPagination={!!serverPagination}
|
||||
rowCount={rowCount}
|
||||
/>
|
||||
</div>
|
||||
</StyledSpace>
|
||||
) : null}
|
||||
{renderTimeComparisonDropdown ? (
|
||||
<div
|
||||
@@ -400,7 +508,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
{renderTimeComparisonDropdown()}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</StyledRow>
|
||||
</div>
|
||||
) : null}
|
||||
{wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()}
|
||||
|
||||
@@ -16,7 +16,13 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { memo, ComponentType, ChangeEventHandler } from 'react';
|
||||
import {
|
||||
memo,
|
||||
ComponentType,
|
||||
ChangeEventHandler,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { Row, FilterValue } from 'react-table';
|
||||
import useAsyncState from '../utils/useAsyncState';
|
||||
|
||||
@@ -24,8 +30,12 @@ export interface SearchInputProps {
|
||||
count: number;
|
||||
value: string;
|
||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||
onBlur?: () => void;
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const isSearchFocused = new Map();
|
||||
|
||||
export interface GlobalFilterProps<D extends object> {
|
||||
preGlobalFilteredRows: Row<D>[];
|
||||
// filter value cannot be `undefined` otherwise React will report component
|
||||
@@ -33,17 +43,28 @@ export interface GlobalFilterProps<D extends object> {
|
||||
filterValue: string;
|
||||
setGlobalFilter: (filterValue: FilterValue) => void;
|
||||
searchInput?: ComponentType<SearchInputProps>;
|
||||
id?: string;
|
||||
serverPagination: boolean;
|
||||
rowCount: number;
|
||||
}
|
||||
|
||||
function DefaultSearchInput({ count, value, onChange }: SearchInputProps) {
|
||||
function DefaultSearchInput({
|
||||
count,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
inputRef,
|
||||
}: SearchInputProps) {
|
||||
return (
|
||||
<span className="dt-global-filter">
|
||||
Search{' '}
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="form-control input-sm"
|
||||
placeholder={`${count} records...`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
@@ -56,8 +77,13 @@ export default (memo as <T>(fn: T) => T)(function GlobalFilter<
|
||||
filterValue = '',
|
||||
searchInput,
|
||||
setGlobalFilter,
|
||||
id = '',
|
||||
serverPagination,
|
||||
rowCount,
|
||||
}: GlobalFilterProps<D>) {
|
||||
const count = preGlobalFilteredRows.length;
|
||||
const count = serverPagination ? rowCount : preGlobalFilteredRows.length;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [value, setValue] = useAsyncState(
|
||||
filterValue,
|
||||
(newValue: string) => {
|
||||
@@ -66,17 +92,37 @@ export default (memo as <T>(fn: T) => T)(function GlobalFilter<
|
||||
200,
|
||||
);
|
||||
|
||||
// Preserve focus during server-side filtering to maintain a better user experience
|
||||
useEffect(() => {
|
||||
if (
|
||||
serverPagination &&
|
||||
isSearchFocused.get(id) &&
|
||||
document.activeElement !== inputRef.current
|
||||
) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [value, serverPagination]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
e.preventDefault();
|
||||
isSearchFocused.set(id, true);
|
||||
setValue(target.value);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
isSearchFocused.set(id, false);
|
||||
};
|
||||
|
||||
const SearchInput = searchInput || DefaultSearchInput;
|
||||
|
||||
return (
|
||||
<SearchInput
|
||||
count={count}
|
||||
value={value}
|
||||
onChange={e => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
e.preventDefault();
|
||||
setValue(target.value);
|
||||
}}
|
||||
inputRef={inputRef}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>,
|
||||
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 { TableOwnState } from '../types/react-table';
|
||||
|
||||
export const updateExternalFormData = (
|
||||
setDataMask: SetDataMaskHook = () => {},
|
||||
@@ -30,3 +31,11 @@ export const updateExternalFormData = (
|
||||
pageSize,
|
||||
},
|
||||
});
|
||||
|
||||
export const updateTableOwnState = (
|
||||
setDataMask: SetDataMaskHook = () => {},
|
||||
modifiedOwnState: TableOwnState,
|
||||
) =>
|
||||
setDataMask({
|
||||
ownState: modifiedOwnState,
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user