mirror of
https://github.com/apache/superset.git
synced 2026-06-15 04:29:18 +00:00
Compare commits
24 Commits
fix_exampl
...
dataset-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bfc128e20 | ||
|
|
ca9e74edd8 | ||
|
|
39b3de6b5d | ||
|
|
26563bb330 | ||
|
|
0653e123cc | ||
|
|
76358ed64e | ||
|
|
217f11a8f7 | ||
|
|
af21ef2497 | ||
|
|
51c25831e8 | ||
|
|
be41e0526a | ||
|
|
0f240ea1b2 | ||
|
|
e520538af6 | ||
|
|
e03d840d06 | ||
|
|
1921ba993e | ||
|
|
b050897ebd | ||
|
|
0bdd8a223d | ||
|
|
d12f86363f | ||
|
|
9f680a63f8 | ||
|
|
928a052440 | ||
|
|
fbc84a1f9a | ||
|
|
fa1693dc5f | ||
|
|
8a8fb49617 | ||
|
|
dc4474889d | ||
|
|
29ac507d56 |
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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -64,6 +64,51 @@ check the [supersetbot docker](https://github.com/apache-superset/supersetbot)
|
||||
subcommand and the [docker.yml](https://github.com/apache/superset/blob/master/.github/workflows/docker.yml)
|
||||
GitHub action.
|
||||
|
||||
## Building your own production Docker image
|
||||
|
||||
Every Superset deployment will require its own set of drivers depending on the data warehouse(s),
|
||||
etc. so we recommend that users build their own Docker image by extending the `lean` image.
|
||||
|
||||
Here's an example Dockerfile that does this. Follow the in-line comments to customize it for
|
||||
your desired Superset version and database drivers. The comments also note that a certain feature flag will
|
||||
have to be enabled in your config file.
|
||||
|
||||
You would build the image with `docker build -t mysuperset:latest .` or `docker build -t ourcompanysuperset:4.1.2 .`
|
||||
|
||||
```Dockerfile
|
||||
# change this to apache/superset:4.1.2 or whatever version you want to build from;
|
||||
# otherwise the default is the latest commit on GitHub master branch
|
||||
FROM apache/superset:master
|
||||
|
||||
USER root
|
||||
|
||||
# Set environment variable for Playwright
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/playwright-browsers
|
||||
|
||||
# Install packages using uv into the virtual environment
|
||||
# Superset started using uv after the 4.1 branch; if you are building from apache/superset:4.1.x,
|
||||
# replace the first two lines with RUN pip install \
|
||||
RUN . /app/.venv/bin/activate && \
|
||||
uv pip install \
|
||||
# install psycopg2 for using PostgreSQL metadata store - could be a MySQL package if using that backend:
|
||||
psycopg2-binary \
|
||||
# add the driver(s) for your data warehouse(s), in this example we're showing for Microsoft SQL Server:
|
||||
pymssql \
|
||||
# package needed for using single-sign on authentication:
|
||||
Authlib \
|
||||
# install Playwright for taking screenshots for Alerts & Reports. This assumes the feature flag PLAYWRIGHT_REPORTS_AND_THUMBNAILS is enabled
|
||||
# That feature flag will default to True starting in 6.0.0
|
||||
# Playwright works only with Chrome.
|
||||
# If you are still using Selenium instead of Playwright, you would instead install here the selenium package and a headless browser & webdriver
|
||||
playwright \
|
||||
&& PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/playwright-browsers playwright install chromium
|
||||
|
||||
# Switch back to the superset user
|
||||
USER superset
|
||||
|
||||
CMD ["/app/docker/entrypoints/run-server.sh"]
|
||||
```
|
||||
|
||||
## Key ARGs in Dockerfile
|
||||
|
||||
- `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,12 @@
|
||||
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 |
|
||||
|
||||
#### Version 4.1.0
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@emotion/styled": "^10.0.27",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@superset-ui/style": "^0.14.23",
|
||||
"antd": "^5.24.9",
|
||||
"antd": "^5.25.1",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"less": "^4.3.0",
|
||||
"less-loader": "^11.0.0",
|
||||
@@ -44,12 +44,12 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-config-prettier": "^10.1.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.8"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -4179,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.9:
|
||||
version "5.24.9"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.24.9.tgz#c5862e02ed770bd95e312961f4f0b7b158a004d9"
|
||||
integrity sha512-liB+Y/JwD5/KSKbK1Z1EVAbWcoWYvWJ1s97AbbT+mOdigpJQuWwH7kG8IXNEljI7onvj0DdD43TXhSRLUu9AMA==
|
||||
antd@^5.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"
|
||||
@@ -4199,7 +4199,7 @@ antd@^5.24.9:
|
||||
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"
|
||||
@@ -4219,7 +4219,7 @@ antd@^5.24.9:
|
||||
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"
|
||||
@@ -4229,7 +4229,7 @@ antd@^5.24.9:
|
||||
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"
|
||||
@@ -6247,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"
|
||||
@@ -10605,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"
|
||||
@@ -10821,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"
|
||||
@@ -10929,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"
|
||||
@@ -13033,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.8:
|
||||
version "5.99.8"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.8.tgz#dd31a020b7c092d30c4c6d9a4edb95809e7f5946"
|
||||
integrity sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.6"
|
||||
|
||||
@@ -44,7 +44,7 @@ dependencies = [
|
||||
"cryptography>=42.0.4, <45.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <3.0.0",
|
||||
"flask-appbuilder>=4.6.3, <5.0.0",
|
||||
"flask-appbuilder>=4.6.4, <5.0.0",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
@@ -240,6 +240,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 +278,6 @@ exclude = [
|
||||
"venv",
|
||||
]
|
||||
|
||||
|
||||
# Same as Black.
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
@@ -367,6 +372,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 +386,7 @@ authorized_licenses = [
|
||||
"osi approved",
|
||||
"psf-2.0",
|
||||
"python software foundation",
|
||||
"simplified bsd",
|
||||
"the unlicense (unlicense)",
|
||||
"the unlicense",
|
||||
]
|
||||
|
||||
@@ -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,7 +8,7 @@ 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
|
||||
@@ -32,7 +32,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 +42,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 +54,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,7 +99,7 @@ email-validator==2.2.0
|
||||
# via flask-appbuilder
|
||||
et-xmlfile==2.0.0
|
||||
# via openpyxl
|
||||
exceptiongroup==1.2.2
|
||||
exceptiongroup==1.3.0
|
||||
# via
|
||||
# cattrs
|
||||
# trio
|
||||
@@ -118,7 +118,7 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.6.3
|
||||
flask-appbuilder==4.6.4
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-babel==2.0.0
|
||||
# via flask-appbuilder
|
||||
@@ -152,13 +152,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 +173,7 @@ idna==3.10
|
||||
# email-validator
|
||||
# requests
|
||||
# trio
|
||||
# url-normalize
|
||||
importlib-metadata==8.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
isodate==0.7.2
|
||||
@@ -190,7 +190,7 @@ jsonpath-ng==1.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
jsonschema==4.23.0
|
||||
# via flask-appbuilder
|
||||
jsonschema-specifications==2024.10.1
|
||||
jsonschema-specifications==2025.4.1
|
||||
# via jsonschema
|
||||
kombu==5.5.3
|
||||
# via celery
|
||||
@@ -243,7 +243,9 @@ openpyxl==3.1.5
|
||||
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 +265,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 +281,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
|
||||
@@ -336,13 +338,13 @@ requests-cache==1.2.1
|
||||
# via shillelagh
|
||||
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,7 +354,6 @@ six==1.17.0
|
||||
# via
|
||||
# prison
|
||||
# python-dateutil
|
||||
# url-normalize
|
||||
# wtforms-json
|
||||
slack-sdk==3.35.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -373,7 +374,7 @@ sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
sqlglot==26.16.4
|
||||
sqlglot==26.17.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
sqlparse==0.5.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -381,17 +382,18 @@ sshtunnel==0.4.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
tabulate==0.8.10
|
||||
# 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
|
||||
# exceptiongroup
|
||||
# limits
|
||||
# pyopenssl
|
||||
# referencing
|
||||
@@ -402,7 +404,7 @@ 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,7 +14,7 @@ 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
|
||||
@@ -51,7 +51,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 +68,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 +76,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 +88,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,7 +176,7 @@ et-xmlfile==2.0.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# openpyxl
|
||||
exceptiongroup==1.2.2
|
||||
exceptiongroup==1.3.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# cattrs
|
||||
@@ -202,7 +202,7 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.6.3
|
||||
flask-appbuilder==4.6.4
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -280,7 +280,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 +318,6 @@ greenlet==3.1.1
|
||||
# apache-superset
|
||||
# gevent
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
grpcio==1.71.0
|
||||
# via
|
||||
# apache-superset
|
||||
@@ -355,6 +354,7 @@ idna==3.10
|
||||
# email-validator
|
||||
# requests
|
||||
# trio
|
||||
# url-normalize
|
||||
importlib-metadata==8.7.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -389,7 +389,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
|
||||
@@ -495,6 +495,7 @@ outcome==1.3.0.post0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# trio
|
||||
# trio-websocket
|
||||
packaging==25.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -542,7 +543,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 +599,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
|
||||
@@ -731,22 +732,22 @@ 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 +768,6 @@ six==1.17.0
|
||||
# prison
|
||||
# python-dateutil
|
||||
# rfc3339-validator
|
||||
# url-normalize
|
||||
# wtforms-json
|
||||
slack-sdk==3.35.0
|
||||
# via
|
||||
@@ -799,7 +799,7 @@ 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
|
||||
@@ -829,21 +829,22 @@ tqdm==4.67.1
|
||||
# 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
|
||||
# exceptiongroup
|
||||
# limits
|
||||
# pyopenssl
|
||||
# referencing
|
||||
@@ -857,7 +858,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
|
||||
|
||||
850
superset-frontend/package-lock.json
generated
850
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -122,7 +122,7 @@
|
||||
"@visx/tooltip": "^3.0.0",
|
||||
"@visx/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",
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -21,10 +21,18 @@ import { t } from '../translation';
|
||||
export default function validateServerPagination(
|
||||
v: unknown,
|
||||
serverPagination: boolean,
|
||||
max: number,
|
||||
maxValueWithoutServerPagination: number,
|
||||
maxServer: number,
|
||||
) {
|
||||
if (Number(v) > +max && !serverPagination) {
|
||||
return t('Server pagination needs to be enabled for values over %s', max);
|
||||
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,
|
||||
|
||||
@@ -20,27 +20,134 @@
|
||||
import { validateServerPagination } from '@superset-ui/core';
|
||||
import './setup';
|
||||
|
||||
test('validateServerPagination returns warning message when server pagination is disabled and value exceeds max', () => {
|
||||
expect(validateServerPagination(100001, false, 100000)).toBeTruthy();
|
||||
expect(validateServerPagination('150000', false, 100000)).toBeTruthy();
|
||||
expect(validateServerPagination(200000, false, 100000)).toBeTruthy();
|
||||
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', () => {
|
||||
expect(validateServerPagination(100001, true, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(150000, true, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination('200000', true, 100000)).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 returns false when value is below max', () => {
|
||||
expect(validateServerPagination(50000, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination('75000', false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(99999, false, 100000)).toBeFalsy();
|
||||
test('validateServerPagination handles 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, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(null, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(NaN, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination('invalid', false, 100000)).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
undefined,
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
null,
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
NaN,
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
'invalid',
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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}`;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,6 +113,10 @@ const StyledSpace = styled(Space)`
|
||||
}
|
||||
`;
|
||||
|
||||
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,
|
||||
@@ -447,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
|
||||
@@ -466,7 +470,11 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
) : null}
|
||||
</div>
|
||||
{searchInput ? (
|
||||
<StyledSpace 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>
|
||||
@@ -500,7 +508,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
{renderTimeComparisonDropdown()}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</StyledRow>
|
||||
</div>
|
||||
) : null}
|
||||
{wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()}
|
||||
|
||||
@@ -79,7 +79,7 @@ import DataTable, {
|
||||
|
||||
import Styles from './Styles';
|
||||
import { formatColumnValue } from './utils/formatValue';
|
||||
import { PAGE_SIZE_OPTIONS } from './consts';
|
||||
import { PAGE_SIZE_OPTIONS, SERVER_PAGE_SIZE_OPTIONS } from './consts';
|
||||
import { updateTableOwnState } from './DataTable/utils/externalAPIs';
|
||||
import getScrollBarSize from './DataTable/utils/getScrollBarSize';
|
||||
|
||||
@@ -306,7 +306,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
// only take relevant page size options
|
||||
const pageSizeOptions = useMemo(() => {
|
||||
const getServerPagination = (n: number) => n <= rowCount;
|
||||
return PAGE_SIZE_OPTIONS.filter(([n]) =>
|
||||
return (
|
||||
serverPagination ? SERVER_PAGE_SIZE_OPTIONS : PAGE_SIZE_OPTIONS
|
||||
).filter(([n]) =>
|
||||
serverPagination ? getServerPagination(n) : n <= 2 * data.length,
|
||||
) as SizeOption[];
|
||||
}, [data.length, rowCount, serverPagination]);
|
||||
|
||||
@@ -30,3 +30,7 @@ export const PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
|
||||
100,
|
||||
200,
|
||||
]);
|
||||
|
||||
export const SERVER_PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
|
||||
10, 20, 50, 100, 200,
|
||||
]);
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import { isEmpty, last } from 'lodash';
|
||||
import { PAGE_SIZE_OPTIONS } from './consts';
|
||||
import { PAGE_SIZE_OPTIONS, SERVER_PAGE_SIZE_OPTIONS } from './consts';
|
||||
import { ColorSchemeEnum } from './types';
|
||||
|
||||
function getQueryMode(controls: ControlStateMapping): QueryMode {
|
||||
@@ -364,7 +364,7 @@ const config: ControlPanelConfig = {
|
||||
freeForm: true,
|
||||
label: t('Server Page Length'),
|
||||
default: 10,
|
||||
choices: PAGE_SIZE_OPTIONS,
|
||||
choices: SERVER_PAGE_SIZE_OPTIONS,
|
||||
description: t('Rows per page, 0 means no pagination'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
Boolean(controls?.server_pagination?.value),
|
||||
@@ -397,6 +397,7 @@ const config: ControlPanelConfig = {
|
||||
v,
|
||||
state?.server_pagination,
|
||||
state?.maxValueWithoutServerPagination || DEFAULT_MAX_ROW,
|
||||
state?.maxValue || DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
],
|
||||
// Re run the validations when this control value
|
||||
|
||||
@@ -11,37 +11,23 @@
|
||||
*
|
||||
* 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
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||
* OF ANY KIND, either express or implied. See the License for
|
||||
* the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import Tooltip, { getTooltipHTML } from './Tooltip';
|
||||
import { getTooltipHTML } from './Tooltip';
|
||||
|
||||
test('should render a tooltip', () => {
|
||||
const expected = {
|
||||
title: 'tooltip title',
|
||||
icon: <div>icon</div>,
|
||||
body: <div>body</div>,
|
||||
meta: 'meta',
|
||||
footer: <div>footer</div>,
|
||||
};
|
||||
render(<Tooltip {...expected} />);
|
||||
expect(screen.getByText(expected.title)).toBeInTheDocument();
|
||||
expect(screen.getByText(expected.meta)).toBeInTheDocument();
|
||||
expect(screen.getByText('icon')).toBeInTheDocument();
|
||||
expect(screen.getByText('body')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('returns the tooltip HTML', () => {
|
||||
test('getTooltipHTML returns the expected HTML (string inputs)', () => {
|
||||
const html = getTooltipHTML({
|
||||
title: 'tooltip title',
|
||||
icon: <div>icon</div>,
|
||||
body: <div>body</div>,
|
||||
meta: 'meta',
|
||||
footer: <div>footer</div>,
|
||||
body: 'body text',
|
||||
footer: 'footer note',
|
||||
});
|
||||
|
||||
expect(html).toContain('tooltip-detail');
|
||||
expect(html).toContain('tooltip title');
|
||||
expect(html).toContain('body text');
|
||||
expect(html).toContain('footer note');
|
||||
});
|
||||
|
||||
@@ -16,42 +16,22 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { Tag } from 'src/components';
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
body?: React.ReactNode;
|
||||
meta?: string;
|
||||
footer?: React.ReactNode;
|
||||
title?: string;
|
||||
body?: string;
|
||||
footer?: string;
|
||||
};
|
||||
|
||||
export const Tooltip: React.FC<Props> = ({
|
||||
title,
|
||||
icon,
|
||||
body,
|
||||
meta,
|
||||
footer,
|
||||
}) => (
|
||||
<div className="tooltip-detail">
|
||||
<div className="tooltip-detail-head">
|
||||
<div className="tooltip-detail-title">
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
{meta && (
|
||||
<span className="tooltip-detail-meta">
|
||||
<Tag color="default">{meta}</Tag>
|
||||
</span>
|
||||
)}
|
||||
export function getTooltipHTML({ title, body, footer }: Props): string {
|
||||
const html = `
|
||||
<div class="tooltip-detail">
|
||||
${title ? `<div class="tooltip-detail-title">${title}</div>` : ''}
|
||||
${body ? `<div class="tooltip-detail-body">${body}</div>` : ''}
|
||||
${footer ? `<div class="tooltip-detail-footer">${footer}</div>` : ''}
|
||||
</div>
|
||||
{body && <div className="tooltip-detail-body">{body ?? title}</div>}
|
||||
{footer && <div className="tooltip-detail-footer">{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const getTooltipHTML = (props: Props) =>
|
||||
`${renderToStaticMarkup(<Tooltip {...props} />)}`;
|
||||
|
||||
export default Tooltip;
|
||||
`;
|
||||
return DOMPurify.sanitize(html);
|
||||
}
|
||||
|
||||
@@ -190,50 +190,37 @@ export default function AsyncAceEditor(
|
||||
return (
|
||||
<>
|
||||
<Global
|
||||
key="ace-tooltip-global"
|
||||
styles={css`
|
||||
.ace_tooltip {
|
||||
margin-left: ${supersetTheme.gridUnit * 2}px;
|
||||
padding: 0px;
|
||||
all: unset;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: ${supersetTheme.colors.grayscale.light5};
|
||||
border: 1px solid ${supersetTheme.colors.grayscale.light1};
|
||||
padding: ${supersetTheme.gridUnit}px
|
||||
${supersetTheme.gridUnit * 2}px;
|
||||
line-height: 1.4;
|
||||
max-width: 400px;
|
||||
min-width: 200px;
|
||||
pointer-events: auto;
|
||||
font-size: ${supersetTheme.typography.sizes.m}px;
|
||||
}
|
||||
|
||||
& .tooltip-detail {
|
||||
background-color: ${supersetTheme.colors.grayscale.light5};
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
min-width: ${supersetTheme.gridUnit * 50}px;
|
||||
max-width: ${supersetTheme.gridUnit * 100}px;
|
||||
& .tooltip-detail-head {
|
||||
background-color: ${supersetTheme.colors.grayscale.light4};
|
||||
color: ${supersetTheme.colors.grayscale.dark1};
|
||||
display: flex;
|
||||
column-gap: ${supersetTheme.gridUnit}px;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
& .tooltip-detail-title {
|
||||
display: flex;
|
||||
column-gap: ${supersetTheme.gridUnit}px;
|
||||
font-weight: bold;
|
||||
font-size: ${supersetTheme.typography.sizes.m}px;
|
||||
}
|
||||
& .tooltip-detail-body {
|
||||
word-break: break-word;
|
||||
font-size: ${supersetTheme.typography.sizes.s}px;
|
||||
padding: ${supersetTheme.gridUnit}px;
|
||||
}
|
||||
& .tooltip-detail-head,
|
||||
& .tooltip-detail-body {
|
||||
padding: ${supersetTheme.gridUnit}px
|
||||
${supersetTheme.gridUnit * 2}px;
|
||||
}
|
||||
& .tooltip-detail-footer {
|
||||
border-top: 1px ${supersetTheme.colors.grayscale.light2}
|
||||
solid;
|
||||
padding: 0 ${supersetTheme.gridUnit * 2}px;
|
||||
color: ${supersetTheme.colors.grayscale.dark1};
|
||||
font-size: ${supersetTheme.typography.sizes.xs}px;
|
||||
}
|
||||
& .tooltip-detail-meta {
|
||||
& > .ant-tag {
|
||||
margin-right: 0px;
|
||||
}
|
||||
font-size: ${supersetTheme.typography.sizes.s}px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
BarChartOutlined,
|
||||
BellOutlined,
|
||||
BookOutlined,
|
||||
BulbOutlined,
|
||||
CaretUpOutlined,
|
||||
CaretDownOutlined,
|
||||
CaretLeftOutlined,
|
||||
@@ -61,6 +62,7 @@ import {
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
FallOutlined,
|
||||
FieldNumberOutlined,
|
||||
FieldTimeOutlined,
|
||||
FileImageOutlined,
|
||||
FileOutlined,
|
||||
@@ -90,6 +92,7 @@ import {
|
||||
SaveOutlined,
|
||||
SearchOutlined,
|
||||
SettingOutlined,
|
||||
ShareAltOutlined,
|
||||
StarOutlined,
|
||||
StarFilled,
|
||||
StopOutlined,
|
||||
@@ -131,6 +134,7 @@ const AntdIcons = {
|
||||
BarChartOutlined,
|
||||
BellOutlined,
|
||||
BookOutlined,
|
||||
BulbOutlined,
|
||||
CaretUpOutlined,
|
||||
CaretDownOutlined,
|
||||
CaretLeftOutlined,
|
||||
@@ -162,6 +166,7 @@ const AntdIcons = {
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
FallOutlined,
|
||||
FieldNumberOutlined,
|
||||
FieldTimeOutlined,
|
||||
FileImageOutlined,
|
||||
FileOutlined,
|
||||
@@ -191,6 +196,7 @@ const AntdIcons = {
|
||||
SaveOutlined,
|
||||
SearchOutlined,
|
||||
SettingOutlined,
|
||||
ShareAltOutlined,
|
||||
StarOutlined,
|
||||
StarFilled,
|
||||
StopOutlined,
|
||||
|
||||
@@ -57,6 +57,7 @@ export const useResultsPane = ({
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [responseError, setResponseError] = useState<string>('');
|
||||
const queryCount = metadata?.queryObjectCount ?? 1;
|
||||
const isQueryCountDynamic = metadata?.dynamicQueryObjectCount;
|
||||
|
||||
useEffect(() => {
|
||||
// it's an invalid formData when gets a errorMessage
|
||||
@@ -139,19 +140,21 @@ export const useResultsPane = ({
|
||||
<EmptyState image="document.svg" title={title} />,
|
||||
);
|
||||
}
|
||||
return resultResp
|
||||
.slice(0, queryCount)
|
||||
.map((result, idx) => (
|
||||
<SingleQueryResultPane
|
||||
data={result.data}
|
||||
colnames={result.colnames}
|
||||
coltypes={result.coltypes}
|
||||
rowcount={result.rowcount}
|
||||
dataSize={dataSize}
|
||||
datasourceId={queryFormData.datasource}
|
||||
key={idx}
|
||||
isVisible={isVisible}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
));
|
||||
const resultRespToDisplay = isQueryCountDynamic
|
||||
? resultResp
|
||||
: resultResp.slice(0, queryCount);
|
||||
|
||||
return resultRespToDisplay.map((result, idx) => (
|
||||
<SingleQueryResultPane
|
||||
data={result.data}
|
||||
colnames={result.colnames}
|
||||
coltypes={result.coltypes}
|
||||
rowcount={result.rowcount}
|
||||
dataSize={dataSize}
|
||||
datasourceId={queryFormData.datasource}
|
||||
key={idx}
|
||||
isVisible={isVisible}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
@@ -174,4 +174,33 @@ describe('ResultsPaneOnDashboard', () => {
|
||||
expect(await findByText('Results')).toBeVisible();
|
||||
expect(await findByText('Results 2')).toBeVisible();
|
||||
});
|
||||
|
||||
test('dynamic number of results pane', async () => {
|
||||
const FakeChart = () => <span>test</span>;
|
||||
const metadata = new ChartMetadata({
|
||||
name: 'test-chart',
|
||||
thumbnail: '',
|
||||
dynamicQueryObjectCount: true,
|
||||
});
|
||||
|
||||
const plugin = new ChartPlugin({
|
||||
metadata,
|
||||
Chart: FakeChart,
|
||||
});
|
||||
plugin.configure({ key: VizType.MixedTimeseries }).register();
|
||||
|
||||
const props = createResultsPaneOnDashboardProps({
|
||||
sliceId: 196,
|
||||
vizType: VizType.MixedTimeseries,
|
||||
});
|
||||
const { findByText, queryByText } = render(
|
||||
<ResultsPaneOnDashboard {...props} />,
|
||||
{
|
||||
useRedux: true,
|
||||
},
|
||||
);
|
||||
expect(await findByText('Results')).toBeVisible();
|
||||
expect(await findByText('Results 2')).toBeVisible();
|
||||
expect(queryByText('Results 3')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import NumberControl from '.';
|
||||
|
||||
const mockedProps = {
|
||||
min: -5,
|
||||
max: 10,
|
||||
step: 1,
|
||||
default: 0,
|
||||
};
|
||||
|
||||
test('render', () => {
|
||||
const { container } = render(<NumberControl {...mockedProps} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('type number', async () => {
|
||||
const props = {
|
||||
...mockedProps,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
render(<NumberControl {...props} />);
|
||||
const input = screen.getByRole('spinbutton');
|
||||
await userEvent.type(input, '9');
|
||||
expect(props.onChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.onChange).toHaveBeenLastCalledWith(9);
|
||||
});
|
||||
|
||||
test('type >max', async () => {
|
||||
const props = {
|
||||
...mockedProps,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
render(<NumberControl {...props} />);
|
||||
const input = screen.getByRole('spinbutton');
|
||||
await userEvent.type(input, '20');
|
||||
expect(props.onChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.onChange).toHaveBeenLastCalledWith(2);
|
||||
});
|
||||
|
||||
test('type NaN', async () => {
|
||||
const props = {
|
||||
...mockedProps,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
render(<NumberControl {...props} />);
|
||||
const input = screen.getByRole('spinbutton');
|
||||
await userEvent.type(input, 'not a number');
|
||||
expect(props.onChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { InputNumber } from 'src/components/Input';
|
||||
import ControlHeader, { ControlHeaderProps } from '../../ControlHeader';
|
||||
|
||||
type NumberValueType = number | undefined;
|
||||
|
||||
export interface NumberControlProps extends ControlHeaderProps {
|
||||
onChange?: (value: NumberValueType) => void;
|
||||
value?: NumberValueType;
|
||||
label?: string;
|
||||
description?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const FullWidthDiv = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const FullWidthInputNumber = styled(InputNumber)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
function parseValue(value: string | number | null | undefined) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
const num = Number(value);
|
||||
return Number.isNaN(num) ? undefined : num;
|
||||
}
|
||||
|
||||
export default function NumberControl({
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
...rest
|
||||
}: NumberControlProps) {
|
||||
return (
|
||||
<FullWidthDiv>
|
||||
<ControlHeader {...rest} />
|
||||
<FullWidthInputNumber
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={value => onChange?.(parseValue(value))}
|
||||
disabled={disabled}
|
||||
aria-label={rest.label}
|
||||
/>
|
||||
</FullWidthDiv>
|
||||
);
|
||||
}
|
||||
@@ -19,10 +19,7 @@
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TextArea } from 'src/components/Input';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipProps as TooltipOptions,
|
||||
} from 'src/components/Tooltip';
|
||||
import { Tooltip, TooltipProps } from 'src/components/Tooltip';
|
||||
import { t, withTheme } from '@superset-ui/core';
|
||||
|
||||
import Button from 'src/components/Button';
|
||||
@@ -59,7 +56,7 @@ const propTypes = {
|
||||
'vertical',
|
||||
]),
|
||||
textAreaStyles: PropTypes.object,
|
||||
tooltipOptions: PropTypes.oneOf([null, TooltipOptions]),
|
||||
tooltipOptions: PropTypes.oneOf([null, TooltipProps]),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
|
||||
@@ -53,6 +53,7 @@ import { ComparisonRangeLabel } from './ComparisonRangeLabel';
|
||||
import LayerConfigsControl from './LayerConfigsControl/LayerConfigsControl';
|
||||
import MapViewControl from './MapViewControl/MapViewControl';
|
||||
import ZoomConfigControl from './ZoomConfigControl/ZoomConfigControl';
|
||||
import NumberControl from './NumberControl';
|
||||
|
||||
const controlMap = {
|
||||
AnnotationLayerControl,
|
||||
@@ -90,6 +91,7 @@ const controlMap = {
|
||||
ComparisonRangeLabel,
|
||||
TimeOffsetControl,
|
||||
ZoomConfigControl,
|
||||
NumberControl,
|
||||
...sharedControlComponents,
|
||||
};
|
||||
export default controlMap;
|
||||
|
||||
@@ -36,11 +36,8 @@ export function getColumnKeywords(columns: ColumnMeta[]) {
|
||||
value: column_name,
|
||||
docHTML: getTooltipHTML({
|
||||
title: column_name,
|
||||
meta: type ? `column: ${type}` : 'column',
|
||||
body: `${description ?? ''}`,
|
||||
footer: is_certified ? (
|
||||
<>{t('Certified by %s', certified_by)}</>
|
||||
) : undefined,
|
||||
body: `type: ${type || 'unknown'}<br />${description ? `description: ${description}` : ''}`,
|
||||
footer: is_certified ? t('Certified by %s', certified_by) : undefined,
|
||||
}),
|
||||
score: COLUMN_AUTOCOMPLETE_SCORE,
|
||||
meta: 'column',
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 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 {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { t } from '@superset-ui/core';
|
||||
import * as ace from 'ace-builds';
|
||||
|
||||
import ExtraOptions from './ExtraOptions';
|
||||
import { DatabaseObject } from '../types';
|
||||
|
||||
const defaultDb = {
|
||||
expose_in_sqllab: true,
|
||||
allow_ctas: false,
|
||||
allow_cvas: false,
|
||||
allow_dml: false,
|
||||
allow_run_async: false,
|
||||
cache_timeout: 300,
|
||||
force_ctas_schema: 'public',
|
||||
masked_encrypted_extra: '',
|
||||
server_cert: '',
|
||||
impersonate_user: false,
|
||||
extra: JSON.stringify({
|
||||
cost_estimate_enabled: false,
|
||||
allows_virtual_table_explore: true,
|
||||
disable_data_preview: false,
|
||||
schema_options: { expand_rows: false },
|
||||
metadata_cache_timeout: {
|
||||
schema_cache_timeout: 600,
|
||||
table_cache_timeout: 1200,
|
||||
},
|
||||
disable_drill_to_detail: false,
|
||||
metadata_params: {},
|
||||
engine_params: {},
|
||||
version: '',
|
||||
cancel_query_on_windows_unload: false,
|
||||
}),
|
||||
engine_information: {
|
||||
supports_file_upload: true,
|
||||
supports_dynamic_catalog: true,
|
||||
},
|
||||
configuration_method: '', // added dummy value for configuration_method
|
||||
database_name: 'Test Database', // added dummy value for database_name
|
||||
driver: 'sqlite', // added dummy value for driver
|
||||
id: 1, // added dummy value for id
|
||||
sqlalchemy_uri: 'sqlite:///:memory:', // added dummy value for sqlalchemy_uri
|
||||
parameters: {}, // added dummy value for parameters
|
||||
};
|
||||
|
||||
describe('ExtraOptions Component', () => {
|
||||
const onInputChange = jest.fn();
|
||||
const onTextChange = jest.fn();
|
||||
const onEditorChange = jest.fn();
|
||||
const onExtraInputChange = jest.fn();
|
||||
const onExtraEditorChange = jest.fn();
|
||||
|
||||
const renderComponent = (dbProps = defaultDb, extension = undefined) =>
|
||||
render(
|
||||
<ExtraOptions
|
||||
db={dbProps as unknown as DatabaseObject}
|
||||
onInputChange={onInputChange}
|
||||
onTextChange={onTextChange}
|
||||
onEditorChange={onEditorChange}
|
||||
onExtraInputChange={onExtraInputChange}
|
||||
onExtraEditorChange={onExtraEditorChange}
|
||||
extraExtension={extension}
|
||||
/>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders all main panels', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText(t('SQL Lab'))).toBeInTheDocument();
|
||||
expect(screen.getByText(t('Performance'))).toBeInTheDocument();
|
||||
expect(screen.getByText(t('Security'))).toBeInTheDocument();
|
||||
expect(screen.getByText(t('Other'))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onInputChange when "Expose database in SQL Lab" checkbox is clicked', () => {
|
||||
renderComponent();
|
||||
const sqlLabText = screen.getByText(t('SQL Lab'));
|
||||
fireEvent.click(sqlLabText);
|
||||
|
||||
const checkbox = screen.getByLabelText(t('Expose database in SQL Lab'));
|
||||
fireEvent.click(checkbox);
|
||||
expect(onInputChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onExtraInputChange when "Enable query cost estimation" checkbox is clicked', () => {
|
||||
renderComponent();
|
||||
const sqlLabText = screen.getByText(t('SQL Lab'));
|
||||
fireEvent.click(sqlLabText);
|
||||
const checkbox = screen.getByLabelText(t('Enable query cost estimation'));
|
||||
fireEvent.click(checkbox);
|
||||
expect(onExtraInputChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onExtraEditorChange when metadata_params json editor changes', async () => {
|
||||
renderComponent();
|
||||
|
||||
// Click to open the editor tab/section
|
||||
const otherHeader = screen.getByText(t('Other'));
|
||||
fireEvent.click(otherHeader);
|
||||
|
||||
// Wait for Ace to initialize (in case it's async)
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('#metadata_params')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Grab editor instance by ID or name
|
||||
const editorInstance = ace.edit('metadata_params');
|
||||
|
||||
act(() => {
|
||||
editorInstance.setValue('{"key":"value"}');
|
||||
});
|
||||
|
||||
expect(onExtraEditorChange).toHaveBeenCalledWith({
|
||||
json: '{"key":"value"}',
|
||||
name: 'metadata_params',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
editorInstance.setValue('foo');
|
||||
});
|
||||
|
||||
expect(onExtraEditorChange).toHaveBeenCalledWith({
|
||||
json: 'foo',
|
||||
name: 'metadata_params',
|
||||
});
|
||||
|
||||
// it accepts invalid json strings
|
||||
act(() => {
|
||||
editorInstance.setValue('{"key":"value');
|
||||
});
|
||||
|
||||
expect(onExtraEditorChange).toHaveBeenCalledWith({
|
||||
json: '{"key":"value',
|
||||
name: 'metadata_params',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onTextChange when server certificate textarea is changed', () => {
|
||||
renderComponent();
|
||||
// Click to open the security tab/section
|
||||
const securityHeader = screen.getByText(t('Security'));
|
||||
fireEvent.click(securityHeader);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(t('Enter CA_BUNDLE'));
|
||||
fireEvent.change(textarea, { target: { value: 'new cert' } });
|
||||
expect(onTextChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles input change for schema cache timeout', () => {
|
||||
renderComponent();
|
||||
const performanceHeader = screen.getByText(t('Performance'));
|
||||
fireEvent.click(performanceHeader);
|
||||
const input = screen.getByTestId('schema-cache-timeout-test');
|
||||
fireEvent.change(input, { target: { value: '500' } });
|
||||
expect(onExtraInputChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles input change for table cache timeout', () => {
|
||||
renderComponent();
|
||||
const performanceHeader = screen.getByText(t('Performance'));
|
||||
fireEvent.click(performanceHeader);
|
||||
const input = screen.getByTestId('table-cache-timeout-test');
|
||||
fireEvent.change(input, { target: { value: '1000' } });
|
||||
expect(onExtraInputChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -271,6 +271,7 @@ const ExtraOptions = ({
|
||||
value={db?.cache_timeout || ''}
|
||||
placeholder={t('Enter duration in seconds')}
|
||||
onChange={onInputChange}
|
||||
data-test="cache-timeout-test"
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
@@ -533,7 +534,9 @@ const ExtraOptions = ({
|
||||
value={
|
||||
!Object.keys(extraJson?.metadata_params || {}).length
|
||||
? ''
|
||||
: extraJson?.metadata_params
|
||||
: typeof extraJson?.metadata_params === 'string'
|
||||
? extraJson?.metadata_params
|
||||
: JSON.stringify(extraJson?.metadata_params)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
*/
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import Button from 'src/components/Button';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Icons } from 'src/components/Icons';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { DropdownButton } from 'src/components/DropdownButton';
|
||||
import { t, useTheme } from '@superset-ui/core';
|
||||
import { useSingleViewResource } from 'src/views/CRUD/hooks';
|
||||
import { logEvent } from 'src/logger/actions';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
@@ -85,7 +88,7 @@ function Footer({
|
||||
|
||||
const tooltipText = t('Select a database table.');
|
||||
|
||||
const onSave = () => {
|
||||
const onSave = (chartRedirect = true) => {
|
||||
if (datasetObject) {
|
||||
const data = {
|
||||
database: datasetObject.db?.id,
|
||||
@@ -97,32 +100,55 @@ function Footer({
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
if (typeof response === 'number') {
|
||||
if (typeof response === 'number' && chartRedirect) {
|
||||
logEvent(LOG_ACTIONS_DATASET_CREATION_SUCCESS, datasetObject);
|
||||
// When a dataset is created the response we get is its ID number
|
||||
history.push(`/chart/add/?dataset=${datasetObject.table_name}`);
|
||||
} else {
|
||||
history.push('/tablemodelview/list/');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const CREATE_DATASET_TEXT = t('Create dataset and create chart');
|
||||
const onSaveDatasetOnly = () => {
|
||||
onSave(false);
|
||||
};
|
||||
|
||||
const CREATE_DATASET_AND_CHART_TEXT = t('Create dataset and create chart');
|
||||
const CREATE_DATASET_TEXT = t('Only create dataset');
|
||||
|
||||
const disabledCheck =
|
||||
!datasetObject?.table_name ||
|
||||
!hasColumns ||
|
||||
datasets?.includes(datasetObject?.table_name);
|
||||
|
||||
const overlayMenu = (
|
||||
<Menu>
|
||||
<Menu.Item onClick={onSaveDatasetOnly}>{CREATE_DATASET_TEXT}</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={cancelButtonOnClick}>{t('Cancel')}</Button>
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
<DropdownButton
|
||||
onClick={onSave}
|
||||
dropdownRender={() => overlayMenu}
|
||||
icon={
|
||||
<Icons.DownOutlined
|
||||
iconSize="xs"
|
||||
iconColor={theme.colors.primary.dark2}
|
||||
/>
|
||||
}
|
||||
trigger={['click']}
|
||||
disabled={disabledCheck}
|
||||
tooltip={!datasetObject?.table_name ? tooltipText : undefined}
|
||||
onClick={onSave}
|
||||
>
|
||||
{CREATE_DATASET_TEXT}
|
||||
</Button>
|
||||
{CREATE_DATASET_AND_CHART_TEXT}
|
||||
</DropdownButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -335,7 +335,13 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: Handle the default to first Value case
|
||||
if (filterState.value !== undefined) {
|
||||
// Set the filter state value if it is defined
|
||||
updateDataMask(filterState.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the default to first Value case
|
||||
if (defaultToFirstItem) {
|
||||
// Set to first item if defaultToFirstItem is true
|
||||
const firstItem: SelectValue = data[0]
|
||||
@@ -345,7 +351,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
updateDataMask(firstItem);
|
||||
}
|
||||
} else if (formData?.defaultValue) {
|
||||
// Case 3 : Handle defalut value case
|
||||
// Handle defalut value case
|
||||
updateDataMask(formData.defaultValue);
|
||||
}
|
||||
}, [
|
||||
|
||||
1276
superset-websocket/utils/client-ws-app/package-lock.json
generated
1276
superset-websocket/utils/client-ws-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.7",
|
||||
"debug": "~4.4.0",
|
||||
"express": "~4.21.2",
|
||||
"debug": "~4.4.1",
|
||||
"express": "~5.1.0",
|
||||
"http-errors": "~2.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "~1.10.0",
|
||||
|
||||
@@ -1532,7 +1532,31 @@ class DeckGLMultiLayer(BaseViz):
|
||||
|
||||
slice_ids = self.form_data.get("deck_slices")
|
||||
slices = db.session.query(Slice).filter(Slice.id.in_(slice_ids)).all()
|
||||
|
||||
features: dict[str, list[Any]] = {}
|
||||
|
||||
for slc in slices:
|
||||
form_data = slc.form_data
|
||||
|
||||
form_data["extra_filters"] = self.form_data.get("extra_filters", [])
|
||||
form_data["extra_form_data"] = self.form_data.get("extra_form_data", {})
|
||||
form_data["adhoc_filters"] = self.form_data.get("adhoc_filters")
|
||||
|
||||
viz_type_name = form_data.get("viz_type")
|
||||
viz_class = viz_types.get(viz_type_name)
|
||||
if not viz_class:
|
||||
continue # skip unknown viz types
|
||||
|
||||
viz_instance = viz_class(datasource=slc.datasource, form_data=form_data)
|
||||
payload = viz_instance.get_payload()
|
||||
|
||||
if payload and "data" in payload and "features" in payload["data"]:
|
||||
if viz_type_name not in features:
|
||||
features[viz_type_name] = []
|
||||
features[viz_type_name].extend(payload["data"]["features"])
|
||||
|
||||
return {
|
||||
"features": features,
|
||||
"mapboxApiKey": config["MAPBOX_API_KEY"],
|
||||
"slices": [slc.data for slc in slices],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user