mirror of
https://github.com/apache/superset.git
synced 2026-07-30 10:32:45 +00:00
Compare commits
40 Commits
alpha_pale
...
query-iden
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3ba12ebf | ||
|
|
268d8e8fee | ||
|
|
ae71b837ff | ||
|
|
12dd4a712e | ||
|
|
04714b83c2 | ||
|
|
6070e84944 | ||
|
|
1cd939e612 | ||
|
|
e71a19a19e | ||
|
|
e50e378d8b | ||
|
|
96cb6030c8 | ||
|
|
94d47113ea | ||
|
|
f756cee01b | ||
|
|
e8926f177d | ||
|
|
16f4516903 | ||
|
|
000d353ef3 | ||
|
|
83b6f672ff | ||
|
|
0dc48e9b41 | ||
|
|
fe9eef9198 | ||
|
|
8a8248b575 | ||
|
|
42d9a78777 | ||
|
|
31a15c5162 | ||
|
|
67b21c45df | ||
|
|
b280ab9e1f | ||
|
|
c42be77c25 | ||
|
|
160917eae8 | ||
|
|
68b84acd93 | ||
|
|
0aa48b6564 | ||
|
|
0fc1955049 | ||
|
|
8a704d293b | ||
|
|
f4754641c8 | ||
|
|
7c98c3f4f6 | ||
|
|
5a32777dd0 | ||
|
|
7229e1ccf3 | ||
|
|
30695d75d7 | ||
|
|
75ee4edc6a | ||
|
|
d269e3d187 | ||
|
|
7d0fabe1ab | ||
|
|
9695249976 | ||
|
|
17c1a37afb | ||
|
|
73dfe57ae2 |
18
.github/workflows/check-python-deps.yml
vendored
18
.github/workflows/check-python-deps.yml
vendored
@@ -24,6 +24,12 @@ jobs:
|
||||
submodules: recursive
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Python
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
@@ -33,10 +39,20 @@ jobs:
|
||||
run: ./scripts/uv-pip-compile.sh
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
if: steps.check.outputs.python
|
||||
run: |
|
||||
if [[ -n "$(git diff)" ]]; then
|
||||
echo "Full diff (for logging/debugging):"
|
||||
git diff
|
||||
|
||||
echo "Filtered diff (excluding comments and whitespace):"
|
||||
filtered_diff=$(git diff -U0 | grep '^[-+]' | grep -vE '^[-+]{3}' | grep -vE '^[-+][[:space:]]*#' | grep -vE '^[-+][[:space:]]*$' || true)
|
||||
echo "$filtered_diff"
|
||||
|
||||
if [[ -n "$filtered_diff" ]]; then
|
||||
echo
|
||||
echo "ERROR: The pinned dependencies are not up-to-date."
|
||||
echo "Please run './scripts/uv-pip-compile.sh' and commit the changes."
|
||||
echo "More info: https://github.com/apache/superset/tree/master/requirements"
|
||||
exit 1
|
||||
else
|
||||
echo "Pinned dependencies are up-to-date."
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -80,7 +80,7 @@ yarn-error.log
|
||||
*.min.js
|
||||
test-changelog.md
|
||||
*.tsbuildinfo
|
||||
.venv
|
||||
|
||||
# Ignore package-lock in packages
|
||||
plugins/*/package-lock.json
|
||||
packages/*/package-lock.json
|
||||
|
||||
@@ -24,7 +24,6 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
- [33603](https://github.com/apache/superset/pull/33603) OpenStreetView has been promoted as the new default for Deck.gl visualization since it can be enabled by default without requiring an API key. If you have Mapbox set up and want to disable OpenStreeView in your environment, please follow the steps documented here [https://superset.apache.org/docs/configuration/map-tiles].
|
||||
- [33116](https://github.com/apache/superset/pull/33116) In Echarts Series charts (e.g. Line, Area, Bar, etc.) charts, the `x_axis_sort_series` and `x_axis_sort_series_ascending` form data items have been renamed with `x_axis_sort` and `x_axis_sort_asc`.
|
||||
There's a migration added that can potentially affect a significant number of existing charts.
|
||||
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
|
||||
@@ -56,7 +55,6 @@ assists people when migrating to a new version.
|
||||
- [30284](https://github.com/apache/superset/pull/30284) Deprecated GLOBAL_ASYNC_QUERIES_REDIS_CONFIG in favor of the new GLOBAL_ASYNC_QUERIES_CACHE_BACKEND configuration. To leverage Redis Sentinel, set CACHE_TYPE to RedisSentinelCache, or use RedisCache for standalone Redis
|
||||
- [31961](https://github.com/apache/superset/pull/31961) Upgraded React from version 16.13.1 to 17.0.2. If you are using custom frontend extensions or plugins, you may need to update them to be compatible with React 17.
|
||||
- [31260](https://github.com/apache/superset/pull/31260) Docker images now use `uv pip install` instead of `pip install` to manage the python envrionment. Most docker-based deployments will be affected, whether you derive one of the published images, or have custom bootstrap script that install python libraries (drivers)
|
||||
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
|
||||
|
||||
### Potential Downtime
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
title: Map Tiles
|
||||
sidebar_position: 12
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Map tiles
|
||||
|
||||
Superset uses OSM and Mapbox tiles by default. OSM is free but you still need setting your MAPBOX_API_KEY if you want to use mapbox maps.
|
||||
|
||||
## Setting map tiles
|
||||
|
||||
Map tiles can be set with `DECKGL_BASE_MAP` in your `superset_config.py` or `superset_config_docker.py`
|
||||
For adding your own map tiles, you can use the following format.
|
||||
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
['tile://https://your_personal_url/{z}/{x}/{y}.png', 'MyTile']
|
||||
]
|
||||
```
|
||||
Openstreetmap tiles url can be added without prefix.
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
['https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', 'OpenStreetMap']
|
||||
]
|
||||
```
|
||||
|
||||
Default values are:
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
['https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Streets (OSM)'],
|
||||
['https://tile.osm.ch/osm-swiss-style/{z}/{x}/{y}.png', 'Topography (OSM)'],
|
||||
['mapbox://styles/mapbox/streets-v9', 'Streets'],
|
||||
['mapbox://styles/mapbox/dark-v9', 'Dark'],
|
||||
['mapbox://styles/mapbox/light-v9', 'Light'],
|
||||
['mapbox://styles/mapbox/satellite-streets-v9', 'Satellite Streets'],
|
||||
['mapbox://styles/mapbox/satellite-v9', 'Satellite'],
|
||||
['mapbox://styles/mapbox/outdoors-v9', 'Outdoors'],
|
||||
]
|
||||
```
|
||||
|
||||
It is possible to set only mapbox by removing osm tiles and other way around.
|
||||
|
||||
:::warning
|
||||
Setting `DECKGL_BASE_MAP` overwrite default values
|
||||
:::
|
||||
|
||||
After defining your map tiles, set them in these variables:
|
||||
- `CORS_OPTIONS`
|
||||
- `connect-src` of `TALISMAN_CONFIG` and `TALISMAN_CONFIG_DEV` variables.
|
||||
|
||||
```python
|
||||
ENABLE_CORS = True
|
||||
CORS_OPTIONS: dict[Any, Any] = {
|
||||
"origins": [
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
"https://your_personal_url/{z}/{x}/{y}.png",
|
||||
]
|
||||
}
|
||||
|
||||
.
|
||||
.
|
||||
|
||||
TALISMAN_CONFIG = {
|
||||
"content_security_policy": {
|
||||
...
|
||||
"connect-src": [
|
||||
"'self'",
|
||||
"https://api.mapbox.com",
|
||||
"https://events.mapbox.com",
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
"https://your_personal_url/{z}/{x}/{y}.png",
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -8,11 +8,11 @@ version: 1
|
||||
|
||||
## CORS
|
||||
|
||||
To configure CORS, or cross-origin resource sharing, the following dependency must be installed:
|
||||
|
||||
```python
|
||||
pip install apache_superset[cors]
|
||||
```
|
||||
:::note
|
||||
In Superset versions prior to `5.x` you have to install to install `flask-cors` with `pip install flask-cors` to enable CORS support.
|
||||
:::
|
||||
|
||||
|
||||
The following keys in `superset_config.py` can be specified to configure CORS:
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ Affecting the Docker build process:
|
||||
|
||||
- **SUPERSET_BUILD_TARGET (default=dev):** which --target to build, either `lean` or `dev` are commonly used
|
||||
- **INCLUDE_FIREFOX (default=false):** whether to include the Firefox headless browser in the build
|
||||
- **INCLUDE_CHROMIUM (default=false):** whether to include the Firefox headless browser in the build
|
||||
- **INCLUDE_CHROMIUM (default=false):** whether to include the Chromium headless browser in the build
|
||||
- **BUILD_TRANSLATIONS(default=false):** whether to compile the translations from the .po files available
|
||||
- **SUPERSET_LOAD_EXAMPLES (default=yes):** whether to load the examples into the database upon startup,
|
||||
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`
|
||||
@@ -614,9 +614,6 @@ act --job test-python-38 --secret GITHUB_TOKEN=$GITHUB_TOKEN --event pull_reques
|
||||
|
||||
There is also a utility script included in the Superset codebase to run Python integration tests. The [readme can be found here](https://github.com/apache/superset/tree/master/scripts/tests).
|
||||
|
||||
There is also a utility script included in the Superset codebase to run python integration tests. The [readme can be
|
||||
found here](https://github.com/apache/superset/tree/master/scripts/tests)
|
||||
|
||||
To run all integration tests, for example, run this script from the root directory:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -40,6 +40,7 @@ dependencies = [
|
||||
"click>=8.0.3",
|
||||
"click-option-group",
|
||||
"colorama",
|
||||
"flask-cors>=4.0.2, <7.0",
|
||||
"croniter>=0.3.28",
|
||||
"cron-descriptor",
|
||||
"cryptography>=42.0.4, <45.0.0",
|
||||
@@ -115,7 +116,6 @@ bigquery = [
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=0.5.14, <1.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
cors = ["flask-cors>=4.0.2, <5.0"]
|
||||
crate = ["sqlalchemy-cratedb>=0.40.1, <1"]
|
||||
databend = ["databend-sqlalchemy>=0.3.2, <1.0"]
|
||||
databricks = [
|
||||
|
||||
@@ -7,7 +7,14 @@ To alter the pinned dependency, you can edit/alter the `.in` and `pyproject.toml
|
||||
```bash
|
||||
./scripts/uv-pip-compile.sh
|
||||
```
|
||||
:::warning
|
||||
The pinned dependencies are based on the `current` version of python supported in Superset.
|
||||
Output of `./scripts/uv-pip-compile.sh` may vary slightly based on the python version you are using to run the command.
|
||||
Check the `pyproject.toml` file for the current version of python supported.
|
||||
:::
|
||||
|
||||
This will generate the pinned requirements in the `.txt` files, which will be used in our CI/CD pipelines and in the Docker images.
|
||||
|
||||
We recommend to everyone in the community to use the pinned requirements in their local development environments, to ensure consistency across different environments, though we don't force requirements as part of our python package semantics to allow flexibility for users to install different versions of the dependencies if they wish.
|
||||
|
||||
Note that `development.txt` is a superset of what's in `base.txt`, and all version numbers for shared library should fully match at all times. `translations.txt` is meant as a supplemental file to be used in conjunction with the other requirements files, and is not meant to be used standalone.
|
||||
|
||||
@@ -36,6 +36,3 @@ marshmallow-sqlalchemy>=1.3.0,<1.4.1
|
||||
|
||||
# needed for python 3.12 support
|
||||
openapi-schema-validator>=0.6.3
|
||||
|
||||
# needed when using the flask-cors extension
|
||||
.[cors]
|
||||
|
||||
@@ -154,7 +154,6 @@ greenlet==3.1.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
gunicorn==23.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
h11==0.16.0
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
-e .[development,bigquery,cors,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
|
||||
-e .[development,bigquery,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml requirements/development.in -o requirements/development.txt
|
||||
# uv pip compile requirements/development.in -c requirements/base.txt -o requirements/development.txt
|
||||
-e .
|
||||
# via -r requirements/development.in
|
||||
alembic==1.15.2
|
||||
# via flask-migrate
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-migrate
|
||||
amqp==5.3.1
|
||||
# via kombu
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# kombu
|
||||
apispec==6.6.1
|
||||
# via flask-appbuilder
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
apsw==3.50.1.0
|
||||
# via shillelagh
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# shillelagh
|
||||
astroid==3.3.10
|
||||
# via pylint
|
||||
attrs==25.3.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# cattrs
|
||||
# jsonschema
|
||||
# outcome
|
||||
@@ -21,50 +30,69 @@ attrs==25.3.0
|
||||
# requests-cache
|
||||
# trio
|
||||
babel==2.17.0
|
||||
# via flask-babel
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-babel
|
||||
backoff==2.2.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
bcrypt==4.3.0
|
||||
# via paramiko
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# paramiko
|
||||
billiard==4.2.1
|
||||
# via celery
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# celery
|
||||
blinker==1.9.0
|
||||
# via flask
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask
|
||||
bottleneck==1.5.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
brotli==1.1.0
|
||||
# via flask-compress
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-compress
|
||||
cachelib==0.13.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-caching
|
||||
# flask-session
|
||||
cachetools==5.5.2
|
||||
# via google-auth
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-auth
|
||||
cattrs==25.1.1
|
||||
# via requests-cache
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests-cache
|
||||
celery==5.5.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
certifi==2025.6.15
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests
|
||||
# selenium
|
||||
cffi==1.17.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# cryptography
|
||||
# pynacl
|
||||
cfgv==3.4.0
|
||||
# via pre-commit
|
||||
charset-normalizer==3.4.2
|
||||
# via requests
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests
|
||||
click==8.2.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# celery
|
||||
# click-didyoumean
|
||||
@@ -74,20 +102,26 @@ click==8.2.1
|
||||
# flask
|
||||
# flask-appbuilder
|
||||
click-didyoumean==0.3.1
|
||||
# via celery
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# celery
|
||||
click-option-group==0.5.7
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
click-plugins==1.1.1
|
||||
# via celery
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# celery
|
||||
click-repl==0.3.0
|
||||
# via celery
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# celery
|
||||
cmdstanpy==1.1.0
|
||||
# via prophet
|
||||
colorama==0.4.6
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
contourpy==1.0.7
|
||||
@@ -96,15 +130,15 @@ coverage==7.6.8
|
||||
# via pytest-cov
|
||||
cron-descriptor==1.4.5
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
croniter==6.0.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
cryptography==44.0.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# paramiko
|
||||
# pyopenssl
|
||||
@@ -113,30 +147,40 @@ cycler==0.12.1
|
||||
db-dtypes==1.3.1
|
||||
# via pandas-gbq
|
||||
defusedxml==0.7.1
|
||||
# via odfpy
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# odfpy
|
||||
deprecated==1.2.18
|
||||
# via limits
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# limits
|
||||
deprecation==2.1.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
dill==0.4.0
|
||||
# via pylint
|
||||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
dnspython==2.7.0
|
||||
# via email-validator
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# email-validator
|
||||
docker==7.0.0
|
||||
# via apache-superset
|
||||
email-validator==2.2.0
|
||||
# via flask-appbuilder
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
et-xmlfile==2.0.0
|
||||
# via openpyxl
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# openpyxl
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
flask==2.3.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
# flask-babel
|
||||
@@ -153,50 +197,59 @@ flask==2.3.3
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.8.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
flask-babel==2.0.0
|
||||
# via flask-appbuilder
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
flask-caching==2.3.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
flask-compress==1.17
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
flask-cors==4.0.2
|
||||
# via apache-superset
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
flask-jwt-extended==4.7.1
|
||||
# via flask-appbuilder
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
flask-limiter==3.12
|
||||
# via flask-appbuilder
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
flask-login==0.6.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
flask-migrate==3.1.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
flask-session==0.8.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
flask-sqlalchemy==2.5.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
# flask-migrate
|
||||
flask-talisman==1.1.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
flask-testing==0.8.1
|
||||
# via apache-superset
|
||||
flask-wtf==1.2.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
fonttools==4.55.0
|
||||
@@ -206,10 +259,12 @@ freezegun==1.5.1
|
||||
future==1.0.0
|
||||
# via pyhive
|
||||
geographiclib==2.0
|
||||
# via geopy
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# geopy
|
||||
geopy==2.4.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
gevent==24.2.1
|
||||
# via apache-superset
|
||||
@@ -222,6 +277,7 @@ google-api-core==2.23.0
|
||||
# sqlalchemy-bigquery
|
||||
google-auth==2.40.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-api-core
|
||||
# google-auth-oauthlib
|
||||
# google-cloud-bigquery
|
||||
@@ -253,11 +309,10 @@ googleapis-common-protos==1.66.0
|
||||
# grpcio-status
|
||||
greenlet==3.1.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# gevent
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
grpcio==1.71.0
|
||||
# via
|
||||
# apache-superset
|
||||
@@ -267,27 +322,30 @@ grpcio-status==1.60.1
|
||||
# via google-api-core
|
||||
gunicorn==23.0.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
h11==0.16.0
|
||||
# via wsproto
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# wsproto
|
||||
hashids==1.3.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
holidays==0.25
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# prophet
|
||||
humanize==4.12.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
identify==2.5.36
|
||||
# via pre-commit
|
||||
idna==3.10
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# email-validator
|
||||
# requests
|
||||
# trio
|
||||
@@ -298,24 +356,27 @@ iniconfig==2.0.0
|
||||
# via pytest
|
||||
isodate==0.7.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
isort==6.0.1
|
||||
# via pylint
|
||||
itsdangerous==2.2.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask
|
||||
# flask-wtf
|
||||
jinja2==3.1.6
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask
|
||||
# flask-babel
|
||||
jsonpath-ng==1.7.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
jsonschema==4.23.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
# openapi-schema-validator
|
||||
# openapi-spec-validator
|
||||
@@ -323,66 +384,82 @@ jsonschema-path==0.3.4
|
||||
# via openapi-spec-validator
|
||||
jsonschema-specifications==2025.4.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# jsonschema
|
||||
# openapi-schema-validator
|
||||
kiwisolver==1.4.7
|
||||
# via matplotlib
|
||||
kombu==5.5.3
|
||||
# via celery
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# celery
|
||||
korean-lunar-calendar==0.3.1
|
||||
# via holidays
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# holidays
|
||||
lazy-object-proxy==1.10.0
|
||||
# via openapi-spec-validator
|
||||
limits==5.1.0
|
||||
# via flask-limiter
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-limiter
|
||||
mako==1.3.10
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# alembic
|
||||
# apache-superset
|
||||
markdown==3.8
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# rich
|
||||
markupsafe==3.0.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# jinja2
|
||||
# mako
|
||||
# werkzeug
|
||||
# wtforms
|
||||
marshmallow==3.26.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
# marshmallow-sqlalchemy
|
||||
marshmallow-sqlalchemy==1.4.0
|
||||
# via flask-appbuilder
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
matplotlib==3.9.0
|
||||
# via prophet
|
||||
mccabe==0.7.0
|
||||
# via pylint
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# markdown-it-py
|
||||
msgpack==1.0.8
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
msgspec==0.19.0
|
||||
# via flask-session
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-session
|
||||
mysqlclient==2.2.6
|
||||
# via apache-superset
|
||||
nh3==0.2.21
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
nodeenv==1.8.0
|
||||
# via pre-commit
|
||||
numpy==1.26.4
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# bottleneck
|
||||
# cmdstanpy
|
||||
@@ -395,22 +472,31 @@ numpy==1.26.4
|
||||
oauthlib==3.2.2
|
||||
# via requests-oauthlib
|
||||
odfpy==1.4.1
|
||||
# via pandas
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# pandas
|
||||
openapi-schema-validator==0.6.3
|
||||
# via openapi-spec-validator
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# openapi-spec-validator
|
||||
openapi-spec-validator==0.7.1
|
||||
# via apache-superset
|
||||
openpyxl==3.1.5
|
||||
# via pandas
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# pandas
|
||||
ordered-set==4.1.0
|
||||
# via flask-limiter
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-limiter
|
||||
outcome==1.3.0.post0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# trio
|
||||
# trio-websocket
|
||||
packaging==25.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# apispec
|
||||
# db-dtypes
|
||||
@@ -426,7 +512,7 @@ packaging==25.0
|
||||
# sqlalchemy-bigquery
|
||||
pandas==2.0.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# cmdstanpy
|
||||
# db-dtypes
|
||||
@@ -438,18 +524,18 @@ parameterized==0.9.0
|
||||
# via apache-superset
|
||||
paramiko==3.5.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# sshtunnel
|
||||
parsedatetime==2.6
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
pathable==0.4.3
|
||||
# via jsonschema-path
|
||||
pgsanity==0.2.9
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
pillow==10.3.0
|
||||
# via
|
||||
@@ -457,25 +543,32 @@ pillow==10.3.0
|
||||
# matplotlib
|
||||
platformdirs==4.3.8
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# pylint
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
ply==3.11
|
||||
# via jsonpath-ng
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# jsonpath-ng
|
||||
polyline==2.0.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
pre-commit==4.1.0
|
||||
# via apache-superset
|
||||
prison==0.2.1
|
||||
# via flask-appbuilder
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
progress==1.6
|
||||
# via apache-superset
|
||||
prompt-toolkit==3.0.51
|
||||
# via click-repl
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# click-repl
|
||||
prophet==1.1.5
|
||||
# via apache-superset
|
||||
proto-plus==1.25.0
|
||||
@@ -495,21 +588,25 @@ psycopg2-binary==2.9.6
|
||||
# via apache-superset
|
||||
pyarrow==18.1.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# db-dtypes
|
||||
# pandas-gbq
|
||||
pyasn1==0.6.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# pyasn1-modules
|
||||
# python-ldap
|
||||
# rsa
|
||||
pyasn1-modules==0.4.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-auth
|
||||
# python-ldap
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# cffi
|
||||
pydata-google-auth==1.9.0
|
||||
# via pandas-gbq
|
||||
pydruid==0.6.9
|
||||
@@ -517,30 +614,38 @@ pydruid==0.6.9
|
||||
pyfakefs==5.3.5
|
||||
# via apache-superset
|
||||
pygments==2.19.1
|
||||
# via rich
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# rich
|
||||
pyhive==0.7.0
|
||||
# via apache-superset
|
||||
pyinstrument==4.4.0
|
||||
# via apache-superset
|
||||
pyjwt==2.10.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
# flask-jwt-extended
|
||||
pylint==3.3.7
|
||||
# via apache-superset
|
||||
pynacl==1.5.0
|
||||
# via paramiko
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# paramiko
|
||||
pyopenssl==25.1.0
|
||||
# via shillelagh
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# shillelagh
|
||||
pyparsing==3.2.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# matplotlib
|
||||
pysocks==1.7.1
|
||||
# via urllib3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# urllib3
|
||||
pytest==7.4.4
|
||||
# via
|
||||
# apache-superset
|
||||
@@ -552,7 +657,7 @@ pytest-mock==3.10.0
|
||||
# via apache-superset
|
||||
python-dateutil==2.9.0.post0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# celery
|
||||
# croniter
|
||||
@@ -567,40 +672,45 @@ python-dateutil==2.9.0.post0
|
||||
# trino
|
||||
python-dotenv==1.1.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
python-geohash==0.8.5
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
python-ldap==3.4.4
|
||||
# via apache-superset
|
||||
pytz==2025.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# croniter
|
||||
# flask-babel
|
||||
# pandas
|
||||
# trino
|
||||
pyxlsb==1.0.10
|
||||
# via pandas
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# pandas
|
||||
pyyaml==6.0.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# apispec
|
||||
# jsonschema-path
|
||||
# pre-commit
|
||||
redis==4.6.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
referencing==0.36.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# jsonschema
|
||||
# jsonschema-path
|
||||
# jsonschema-specifications
|
||||
requests==2.32.4
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# docker
|
||||
# google-api-core
|
||||
# google-cloud-bigquery
|
||||
@@ -612,24 +722,33 @@ requests==2.32.4
|
||||
# shillelagh
|
||||
# trino
|
||||
requests-cache==1.2.1
|
||||
# via shillelagh
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# shillelagh
|
||||
requests-oauthlib==2.0.0
|
||||
# via google-auth-oauthlib
|
||||
rfc3339-validator==0.1.4
|
||||
# via openapi-schema-validator
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# openapi-schema-validator
|
||||
rich==13.9.4
|
||||
# via flask-limiter
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-limiter
|
||||
rpds-py==0.25.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# jsonschema
|
||||
# referencing
|
||||
rsa==4.9.1
|
||||
# via google-auth
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-auth
|
||||
ruff==0.8.0
|
||||
# via apache-superset
|
||||
selenium==4.32.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
setuptools==80.7.1
|
||||
# via
|
||||
@@ -640,29 +759,34 @@ setuptools==80.7.1
|
||||
# zope-interface
|
||||
shillelagh==1.3.5
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
simplejson==3.20.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
six==1.17.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# prison
|
||||
# python-dateutil
|
||||
# rfc3339-validator
|
||||
# wtforms-json
|
||||
slack-sdk==3.35.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
sniffio==1.3.1
|
||||
# via trio
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# trio
|
||||
sortedcontainers==2.4.0
|
||||
# via trio
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# trio
|
||||
sqlalchemy==1.4.54
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# alembic
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
@@ -675,24 +799,24 @@ sqlalchemy-bigquery==1.12.0
|
||||
# via apache-superset
|
||||
sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
sqlglot==26.28.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
sqloxide==0.1.51
|
||||
# via apache-superset
|
||||
sshtunnel==0.4.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
statsd==4.0.1
|
||||
# via apache-superset
|
||||
tabulate==0.9.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
tomlkit==0.13.3
|
||||
# via pylint
|
||||
@@ -704,13 +828,16 @@ trino==0.330.0
|
||||
# via apache-superset
|
||||
trio==0.30.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# selenium
|
||||
# trio-websocket
|
||||
trio-websocket==0.12.2
|
||||
# via selenium
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# selenium
|
||||
typing-extensions==4.14.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# alembic
|
||||
# apache-superset
|
||||
# cattrs
|
||||
@@ -721,55 +848,71 @@ typing-extensions==4.14.0
|
||||
# shillelagh
|
||||
tzdata==2025.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# kombu
|
||||
# pandas
|
||||
tzlocal==5.2
|
||||
# via trino
|
||||
url-normalize==2.2.1
|
||||
# via requests-cache
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests-cache
|
||||
urllib3==2.5.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# docker
|
||||
# requests
|
||||
# requests-cache
|
||||
# selenium
|
||||
vine==5.1.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# amqp
|
||||
# celery
|
||||
# kombu
|
||||
virtualenv==20.29.2
|
||||
# via pre-commit
|
||||
wcwidth==0.2.13
|
||||
# via prompt-toolkit
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# prompt-toolkit
|
||||
websocket-client==1.8.0
|
||||
# via selenium
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# selenium
|
||||
werkzeug==3.1.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask
|
||||
# flask-appbuilder
|
||||
# flask-jwt-extended
|
||||
# flask-login
|
||||
wrapt==1.17.2
|
||||
# via deprecated
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# deprecated
|
||||
wsproto==1.2.0
|
||||
# via trio-websocket
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# trio-websocket
|
||||
wtforms==3.2.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
# flask-wtf
|
||||
# wtforms-json
|
||||
wtforms-json==0.3.5
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
xlrd==2.0.1
|
||||
# via pandas
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# pandas
|
||||
xlsxwriter==3.0.9
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# pandas
|
||||
zope-event==5.0
|
||||
@@ -777,4 +920,6 @@ zope-event==5.0
|
||||
zope-interface==5.4.0
|
||||
# via gevent
|
||||
zstandard==0.23.0
|
||||
# via flask-compress
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-compress
|
||||
|
||||
@@ -1,431 +1,4 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml requirements/translations.in -o requirements/translations.txt
|
||||
alembic==1.16.2
|
||||
# via flask-migrate
|
||||
amqp==5.3.1
|
||||
# via kombu
|
||||
apispec==6.8.2
|
||||
# via flask-appbuilder
|
||||
apsw==3.50.2.0
|
||||
# via shillelagh
|
||||
attrs==25.3.0
|
||||
# via
|
||||
# cattrs
|
||||
# jsonschema
|
||||
# outcome
|
||||
# referencing
|
||||
# requests-cache
|
||||
# trio
|
||||
# uv pip compile requirements/translations.in -o requirements/translations.txt
|
||||
babel==2.17.0
|
||||
# via
|
||||
# -r requirements/translations.in
|
||||
# flask-babel
|
||||
backoff==2.2.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
bcrypt==4.3.0
|
||||
# via paramiko
|
||||
billiard==4.2.1
|
||||
# via celery
|
||||
blinker==1.9.0
|
||||
# via flask
|
||||
bottleneck==1.5.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
brotli==1.1.0
|
||||
# via flask-compress
|
||||
cachelib==0.13.0
|
||||
# via
|
||||
# flask-caching
|
||||
# flask-session
|
||||
cachetools==5.5.2
|
||||
# via google-auth
|
||||
cattrs==25.1.1
|
||||
# via requests-cache
|
||||
celery==5.5.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
certifi==2025.6.15
|
||||
# via
|
||||
# requests
|
||||
# selenium
|
||||
cffi==1.17.1
|
||||
# via
|
||||
# cryptography
|
||||
# pynacl
|
||||
charset-normalizer==3.4.2
|
||||
# via requests
|
||||
click==8.2.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# celery
|
||||
# click-didyoumean
|
||||
# click-option-group
|
||||
# click-plugins
|
||||
# click-repl
|
||||
# flask
|
||||
# flask-appbuilder
|
||||
click-didyoumean==0.3.1
|
||||
# via celery
|
||||
click-option-group==0.5.7
|
||||
# via apache-superset (pyproject.toml)
|
||||
click-plugins==1.1.1.2
|
||||
# via celery
|
||||
click-repl==0.3.0
|
||||
# via celery
|
||||
colorama==0.4.6
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
cron-descriptor==1.4.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
croniter==6.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
cryptography==44.0.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# paramiko
|
||||
# pyopenssl
|
||||
defusedxml==0.7.1
|
||||
# via odfpy
|
||||
deprecated==1.2.18
|
||||
# via limits
|
||||
deprecation==2.1.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
dnspython==2.7.0
|
||||
# via email-validator
|
||||
email-validator==2.2.0
|
||||
# via flask-appbuilder
|
||||
et-xmlfile==2.0.0
|
||||
# via openpyxl
|
||||
flask==2.3.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
# flask-babel
|
||||
# flask-caching
|
||||
# flask-compress
|
||||
# flask-jwt-extended
|
||||
# flask-limiter
|
||||
# flask-login
|
||||
# flask-migrate
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.8.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-babel==2.0.0
|
||||
# via flask-appbuilder
|
||||
flask-caching==2.3.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-compress==1.17
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-jwt-extended==4.7.1
|
||||
# via flask-appbuilder
|
||||
flask-limiter==3.12
|
||||
# via flask-appbuilder
|
||||
flask-login==0.6.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
flask-migrate==3.1.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-session==0.8.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-sqlalchemy==2.5.1
|
||||
# via
|
||||
# flask-appbuilder
|
||||
# flask-migrate
|
||||
flask-talisman==1.1.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-wtf==1.2.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
geographiclib==2.0
|
||||
# via geopy
|
||||
geopy==2.4.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
google-auth==2.40.3
|
||||
# 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
|
||||
# via wsproto
|
||||
hashids==1.3.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
holidays==0.25
|
||||
# via apache-superset (pyproject.toml)
|
||||
humanize==4.12.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
idna==3.10
|
||||
# via
|
||||
# email-validator
|
||||
# requests
|
||||
# trio
|
||||
# url-normalize
|
||||
isodate==0.7.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
itsdangerous==2.2.0
|
||||
# via
|
||||
# flask
|
||||
# flask-wtf
|
||||
jinja2==3.1.6
|
||||
# via
|
||||
# flask
|
||||
# flask-babel
|
||||
jsonpath-ng==1.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
jsonschema==4.24.0
|
||||
# via flask-appbuilder
|
||||
jsonschema-specifications==2025.4.1
|
||||
# via jsonschema
|
||||
kombu==5.5.4
|
||||
# via celery
|
||||
korean-lunar-calendar==0.3.1
|
||||
# via holidays
|
||||
limits==5.4.0
|
||||
# via flask-limiter
|
||||
mako==1.3.10
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# alembic
|
||||
markdown==3.8.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
markupsafe==3.0.2
|
||||
# via
|
||||
# jinja2
|
||||
# mako
|
||||
# werkzeug
|
||||
# wtforms
|
||||
marshmallow==3.26.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
# marshmallow-sqlalchemy
|
||||
marshmallow-sqlalchemy==1.4.2
|
||||
# via flask-appbuilder
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
msgpack==1.0.8
|
||||
# via apache-superset (pyproject.toml)
|
||||
msgspec==0.19.0
|
||||
# via flask-session
|
||||
nh3==0.2.21
|
||||
# via apache-superset (pyproject.toml)
|
||||
numpy==2.2.6
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# bottleneck
|
||||
# pandas
|
||||
odfpy==1.4.1
|
||||
# via pandas
|
||||
openpyxl==3.1.5
|
||||
# via pandas
|
||||
ordered-set==4.1.0
|
||||
# via flask-limiter
|
||||
outcome==1.3.0.post0
|
||||
# via
|
||||
# trio
|
||||
# trio-websocket
|
||||
packaging==25.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apispec
|
||||
# deprecation
|
||||
# gunicorn
|
||||
# kombu
|
||||
# limits
|
||||
# marshmallow
|
||||
# shillelagh
|
||||
pandas==2.0.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
paramiko==3.5.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# sshtunnel
|
||||
parsedatetime==2.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
pgsanity==0.2.9
|
||||
# via apache-superset (pyproject.toml)
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
ply==3.11
|
||||
# via jsonpath-ng
|
||||
polyline==2.0.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
prison==0.2.1
|
||||
# via flask-appbuilder
|
||||
prompt-toolkit==3.0.51
|
||||
# via click-repl
|
||||
pyarrow==18.1.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
pyasn1==0.6.1
|
||||
# via
|
||||
# pyasn1-modules
|
||||
# rsa
|
||||
pyasn1-modules==0.4.2
|
||||
# via google-auth
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pygments==2.19.2
|
||||
# via rich
|
||||
pyjwt==2.10.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
# flask-jwt-extended
|
||||
pynacl==1.5.0
|
||||
# via paramiko
|
||||
pyopenssl==25.1.0
|
||||
# via shillelagh
|
||||
pyparsing==3.2.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
pysocks==1.7.1
|
||||
# via urllib3
|
||||
python-dateutil==2.9.0.post0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# celery
|
||||
# croniter
|
||||
# flask-appbuilder
|
||||
# holidays
|
||||
# pandas
|
||||
# shillelagh
|
||||
python-dotenv==1.1.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
python-geohash==0.8.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
pytz==2025.2
|
||||
# via
|
||||
# croniter
|
||||
# flask-babel
|
||||
# pandas
|
||||
pyxlsb==1.0.10
|
||||
# via pandas
|
||||
pyyaml==6.0.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apispec
|
||||
redis==4.6.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
referencing==0.36.2
|
||||
# via
|
||||
# jsonschema
|
||||
# jsonschema-specifications
|
||||
requests==2.32.4
|
||||
# via
|
||||
# requests-cache
|
||||
# shillelagh
|
||||
requests-cache==1.2.1
|
||||
# via shillelagh
|
||||
rich==13.9.4
|
||||
# via flask-limiter
|
||||
rpds-py==0.25.1
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
rsa==4.9.1
|
||||
# via google-auth
|
||||
selenium==4.34.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
shillelagh==1.3.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
simplejson==3.20.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
six==1.17.0
|
||||
# via
|
||||
# prison
|
||||
# python-dateutil
|
||||
# wtforms-json
|
||||
slack-sdk==3.35.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
sniffio==1.3.1
|
||||
# via trio
|
||||
sortedcontainers==2.4.0
|
||||
# via trio
|
||||
sqlalchemy==1.4.54
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# alembic
|
||||
# flask-appbuilder
|
||||
# flask-sqlalchemy
|
||||
# marshmallow-sqlalchemy
|
||||
# shillelagh
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
sqlglot==26.31.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
sshtunnel==0.4.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
tabulate==0.9.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
trio==0.30.0
|
||||
# via
|
||||
# selenium
|
||||
# trio-websocket
|
||||
trio-websocket==0.12.2
|
||||
# via selenium
|
||||
typing-extensions==4.14.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# alembic
|
||||
# cattrs
|
||||
# limits
|
||||
# pyopenssl
|
||||
# referencing
|
||||
# selenium
|
||||
# shillelagh
|
||||
tzdata==2025.2
|
||||
# via
|
||||
# kombu
|
||||
# pandas
|
||||
url-normalize==2.2.1
|
||||
# via requests-cache
|
||||
urllib3==2.4.0
|
||||
# via
|
||||
# requests
|
||||
# requests-cache
|
||||
# selenium
|
||||
vine==5.1.0
|
||||
# via
|
||||
# amqp
|
||||
# celery
|
||||
# kombu
|
||||
wcwidth==0.2.13
|
||||
# via prompt-toolkit
|
||||
websocket-client==1.8.0
|
||||
# via selenium
|
||||
werkzeug==3.1.3
|
||||
# via
|
||||
# flask
|
||||
# flask-appbuilder
|
||||
# flask-jwt-extended
|
||||
# flask-login
|
||||
wrapt==1.17.2
|
||||
# via deprecated
|
||||
wsproto==1.2.0
|
||||
# via trio-websocket
|
||||
wtforms==3.2.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
# flask-wtf
|
||||
# wtforms-json
|
||||
wtforms-json==0.3.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
xlrd==2.0.2
|
||||
# via pandas
|
||||
xlsxwriter==3.0.9
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# pandas
|
||||
zstandard==0.23.0
|
||||
# via flask-compress
|
||||
# via -r requirements/translations.in
|
||||
|
||||
@@ -24,7 +24,8 @@ ADDITIONAL_ARGS="$@"
|
||||
# Generate the requirements/base.txt file
|
||||
uv pip compile pyproject.toml requirements/base.in -o requirements/base.txt $ADDITIONAL_ARGS
|
||||
|
||||
# Generate the requirements/development.txt file, making sure requirements/base.txt is a constraint to keep the versions in sync
|
||||
# Generate the requirements/development.txt file, making sure requirements/base.txt is a constraint to keep the versions in sync. Note that `development.txt` is a Superset of `base.txt` where version for the shared libs should match their version.
|
||||
uv pip compile requirements/development.in -c requirements/base.txt -o requirements/development.txt $ADDITIONAL_ARGS
|
||||
|
||||
# NOTE translation is intended as a "supplemental" set of pins that can be combined with either base or dev as needed
|
||||
uv pip compile requirements/translations.in -o requirements/translations.txt $ADDITIONAL_ARGS
|
||||
|
||||
@@ -46,10 +46,10 @@ module.exports = {
|
||||
plugins: [
|
||||
'lodash',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||
['@babel/plugin-proposal-optional-chaining', { loose: true }],
|
||||
['@babel/plugin-proposal-private-methods', { loose: true }],
|
||||
['@babel/plugin-proposal-nullish-coalescing-operator', { loose: true }],
|
||||
['@babel/plugin-transform-class-properties', { loose: true }],
|
||||
['@babel/plugin-transform-optional-chaining', { loose: true }],
|
||||
['@babel/plugin-transform-private-methods', { loose: true }],
|
||||
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],
|
||||
['@babel/plugin-transform-runtime', { corejs: 3 }],
|
||||
// only used in packages/superset-ui-core/src/chart/components/reactify.tsx
|
||||
['babel-plugin-typescript-to-proptypes', { loose: true }],
|
||||
|
||||
@@ -56,7 +56,7 @@ module.exports = {
|
||||
],
|
||||
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|jest-enzyme|geostyler|geostyler-.*)',
|
||||
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
|
||||
1608
superset-frontend/package-lock.json
generated
1608
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -176,7 +176,7 @@
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-json-tree": "^0.17.0",
|
||||
"react-json-tree": "^0.20.0",
|
||||
"react-lines-ellipsis": "^0.15.4",
|
||||
"react-loadable": "^5.5.0",
|
||||
"react-redux": "^7.2.9",
|
||||
@@ -212,10 +212,6 @@
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@babel/node": "^7.22.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
||||
"@babel/plugin-transform-runtime": "^7.27.1",
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface Dataset {
|
||||
database?: Record<string, unknown>;
|
||||
normalize_columns?: boolean;
|
||||
always_filter_main_dttm?: boolean;
|
||||
extra?: object | string;
|
||||
}
|
||||
|
||||
export interface ControlPanelState {
|
||||
@@ -161,6 +162,7 @@ export type InternalControlType =
|
||||
| 'DatasourceControl'
|
||||
| 'DateFilterControl'
|
||||
| 'FixedOrMetricControl'
|
||||
| 'ColorBreakpointsControl'
|
||||
| 'HiddenControl'
|
||||
| 'SelectAsyncControl'
|
||||
| 'SelectControl'
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
||||
@@ -62,12 +62,12 @@ test('Calling "onHide"', async () => {
|
||||
expect(props.onConfirm).toHaveBeenCalledTimes(0);
|
||||
|
||||
// type "del" in the input
|
||||
await userEvent.type(screen.getByTestId('delete-modal-input'), 'del');
|
||||
userEvent.type(screen.getByTestId('delete-modal-input'), 'del');
|
||||
expect(screen.getByTestId('delete-modal-input')).toHaveValue('del');
|
||||
|
||||
// close the modal
|
||||
expect(screen.getByText('×')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('×'));
|
||||
expect(screen.getByTestId('close-modal-btn')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId('close-modal-btn'));
|
||||
expect(props.onHide).toHaveBeenCalledTimes(1);
|
||||
expect(props.onConfirm).toHaveBeenCalledTimes(0);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { isValidElement, cloneElement, useMemo, useRef, useState } from 'react';
|
||||
import { isNil } from 'lodash';
|
||||
import { css, styled, t } from '@superset-ui/core';
|
||||
import { css, styled, t, useTheme } from '@superset-ui/core';
|
||||
import { Modal as AntdModal, ModalProps as AntdModalProps } from 'antd';
|
||||
import { Resizable } from 're-resizable';
|
||||
import Draggable, {
|
||||
@@ -26,6 +26,7 @@ import Draggable, {
|
||||
DraggableData,
|
||||
DraggableEvent,
|
||||
} from 'react-draggable';
|
||||
import { Icons } from '../Icons';
|
||||
import { Button } from '../Button';
|
||||
import type { ModalProps, StyledModalProps } from './types';
|
||||
|
||||
@@ -45,8 +46,16 @@ export const BaseModal = (props: AntdModalProps) => (
|
||||
);
|
||||
|
||||
export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
${({ theme, responsive, maxWidth }) =>
|
||||
responsive &&
|
||||
${({
|
||||
theme,
|
||||
responsive,
|
||||
maxWidth,
|
||||
resizable,
|
||||
height,
|
||||
draggable,
|
||||
hideFooter,
|
||||
}) => css`
|
||||
${responsive &&
|
||||
css`
|
||||
max-width: ${maxWidth ?? '900px'};
|
||||
padding-left: ${theme.sizeUnit * 3}px;
|
||||
@@ -55,120 +64,120 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
top: 0;
|
||||
`}
|
||||
|
||||
.ant-modal-content {
|
||||
background-color: ${({ theme }) => theme.colorBgContainer};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: ${({ theme }) => `calc(100vh - ${theme.sizeUnit * 8}px)`};
|
||||
margin-bottom: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
margin-top: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
flex: 0 0 auto;
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px
|
||||
${({ theme }) => theme.borderRadius}px 0 0;
|
||||
padding: ${({ theme }) => theme.sizeUnit * 4}px
|
||||
${({ theme }) => theme.sizeUnit * 6}px;
|
||||
|
||||
.ant-modal-title {
|
||||
font-weight: ${({ theme }) => theme.fontWeightStrong};
|
||||
}
|
||||
|
||||
.ant-modal-title h4 {
|
||||
.ant-modal-content {
|
||||
background-color: ${theme.colorBgContainer};
|
||||
display: flex;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
width: ${({ theme }) => theme.sizeUnit * 14}px;
|
||||
height: ${({ theme }) => theme.sizeUnit * 14}px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.ant-modal-close:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ant-modal-close-x {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.close {
|
||||
flex: 1 1 auto;
|
||||
margin-bottom: ${({ theme }) => theme.sizeUnit}px;
|
||||
color: ${({ theme }) => theme.colorPrimaryText};
|
||||
font-size: 32px;
|
||||
font-weight: ${({ theme }) => theme.fontWeightLight};
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
flex: 0 1 auto;
|
||||
padding: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
overflow: auto;
|
||||
${({ resizable, height }) => !resizable && height && `height: ${height};`}
|
||||
}
|
||||
.ant-modal-footer {
|
||||
flex: 0 0 1;
|
||||
border-top: ${({ theme }) => theme.sizeUnit / 4}px solid
|
||||
${({ theme }) => theme.colorSplit};
|
||||
padding: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
margin-top: 0;
|
||||
|
||||
.btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn + .btn {
|
||||
margin-left: ${({ theme }) => theme.sizeUnit * 2}px;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-content-padding .ant-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
${({ draggable, theme }) =>
|
||||
draggable &&
|
||||
`
|
||||
.ant-modal-header {
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - ${theme.sizeUnit * 8}px);
|
||||
margin-bottom: ${theme.sizeUnit * 4}px;
|
||||
margin-top: ${theme.sizeUnit * 4}px;
|
||||
padding: 0;
|
||||
.draggable-trigger {
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
flex: 0 0 auto;
|
||||
border-radius: ${theme.borderRadius}px ${theme.borderRadius}px 0 0;
|
||||
padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 6}px;
|
||||
|
||||
.ant-modal-title {
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
}
|
||||
|
||||
.ant-modal-title h4 {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
width: ${theme.sizeUnit * 14}px;
|
||||
height: ${theme.sizeUnit * 14}px;
|
||||
padding: ${theme.sizeUnit * 6}px ${theme.sizeUnit * 4}px
|
||||
${theme.sizeUnit * 4}px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ant-modal-close:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ant-modal-close-x {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
[data-test='close-modal-btn'] {
|
||||
justify-content: center;
|
||||
}
|
||||
.close {
|
||||
flex: 1 1 auto;
|
||||
margin-bottom: ${theme.sizeUnit}px;
|
||||
color: ${theme.colorPrimaryText};
|
||||
font-weight: ${theme.fontWeightLight};
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
flex: 0 1 auto;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
overflow: auto;
|
||||
${!resizable && height && `height: ${height};`}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
flex: 0 0 1;
|
||||
border-top: ${theme.sizeUnit / 4}px solid ${theme.colorSplit};
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
margin-top: 0;
|
||||
|
||||
.btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn + .btn {
|
||||
margin-left: ${theme.sizeUnit * 2}px;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-content-padding .ant-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
${draggable &&
|
||||
css`
|
||||
.ant-modal-header {
|
||||
padding: 0;
|
||||
|
||||
.draggable-trigger {
|
||||
cursor: move;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`};
|
||||
|
||||
${({ resizable, hideFooter }) =>
|
||||
resizable &&
|
||||
`
|
||||
.resizable {
|
||||
pointer-events: all;
|
||||
|
||||
.resizable-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
`}
|
||||
|
||||
.ant-modal-content {
|
||||
height: 100%;
|
||||
${resizable &&
|
||||
css`
|
||||
.resizable {
|
||||
pointer-events: all;
|
||||
|
||||
.ant-modal-body {
|
||||
/* 100% - header height - footer height */
|
||||
height: ${
|
||||
hideFooter
|
||||
? `calc(100% - ${MODAL_HEADER_HEIGHT}px);`
|
||||
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px);`
|
||||
.resizable-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
height: 100%;
|
||||
|
||||
.ant-modal-body {
|
||||
height: ${hideFooter
|
||||
? `calc(100% - ${MODAL_HEADER_HEIGHT}px)`
|
||||
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px)`};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -221,6 +230,7 @@ const CustomModal = ({
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
const [bounds, setBounds] = useState<DraggableBounds>();
|
||||
const [dragDisabled, setDragDisabled] = useState<boolean>(true);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleOnHide = () => {
|
||||
openerRef?.current?.focus();
|
||||
@@ -312,9 +322,13 @@ const CustomModal = ({
|
||||
open={show}
|
||||
title={<ModalTitle />}
|
||||
closeIcon={
|
||||
<span data-test="close-modal-btn" className="close" aria-hidden="true">
|
||||
×
|
||||
</span>
|
||||
<Icons.CloseOutlined
|
||||
iconColor={theme.colorText}
|
||||
iconSize="l"
|
||||
data-test="close-modal-btn"
|
||||
className="close"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
footer={!hideFooter ? modalFooter : null}
|
||||
hideFooter={hideFooter}
|
||||
|
||||
@@ -45,7 +45,7 @@ interface HandleSelectProps {
|
||||
}
|
||||
|
||||
const menuItemStyles = (theme: any) => css`
|
||||
&.antd5-menu-item {
|
||||
&.ant-menu-item {
|
||||
height: auto;
|
||||
line-height: 1.4;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
Radio as Antd5Radio,
|
||||
Radio as AntRadio,
|
||||
type CheckboxOptionType,
|
||||
type RadioGroupProps,
|
||||
} from 'antd';
|
||||
@@ -40,19 +40,19 @@ const RadioGroup = ({
|
||||
...props
|
||||
}: RadioGroupWrapperProps) => {
|
||||
const content = options.map((option: CheckboxOptionType) => (
|
||||
<Antd5Radio key={option.value} value={option.value}>
|
||||
<AntRadio key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Antd5Radio>
|
||||
</AntRadio>
|
||||
));
|
||||
return (
|
||||
<Antd5Radio.Group {...props}>
|
||||
<AntRadio.Group {...props}>
|
||||
{spaceConfig ? <Space {...spaceConfig}>{content}</Space> : content}
|
||||
</Antd5Radio.Group>
|
||||
</AntRadio.Group>
|
||||
);
|
||||
};
|
||||
export const Radio = Object.assign(Antd5Radio, {
|
||||
export const Radio = Object.assign(AntRadio, {
|
||||
GroupWrapper: RadioGroup,
|
||||
Button: Antd5Radio.Button,
|
||||
Button: AntRadio.Button,
|
||||
});
|
||||
export type {
|
||||
RadioChangeEvent,
|
||||
|
||||
@@ -19,28 +19,28 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { Dropdown, Icons } from '@superset-ui/core/components';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { ThemeMode } from '../../theme/types';
|
||||
import { ThemeAlgorithm, ThemeMode } from '../../theme/types';
|
||||
|
||||
export interface ThemeSelectProps {
|
||||
changeThemeMode: (newMode: ThemeMode) => void;
|
||||
setThemeMode: (newMode: ThemeMode) => void;
|
||||
tooltipTitle?: string;
|
||||
themeMode: ThemeMode;
|
||||
}
|
||||
|
||||
const ThemeSelect: React.FC<ThemeSelectProps> = ({
|
||||
changeThemeMode,
|
||||
setThemeMode,
|
||||
tooltipTitle = 'Select theme',
|
||||
themeMode,
|
||||
}) => {
|
||||
const handleSelect = (mode: ThemeMode) => {
|
||||
changeThemeMode(mode);
|
||||
setThemeMode(mode);
|
||||
};
|
||||
|
||||
const themeIconMap: Record<ThemeMode, React.ReactNode> = {
|
||||
[ThemeMode.LIGHT]: <Icons.SunOutlined />,
|
||||
[ThemeMode.DARK]: <Icons.MoonOutlined />,
|
||||
const themeIconMap: Record<ThemeAlgorithm | ThemeMode, React.ReactNode> = {
|
||||
[ThemeAlgorithm.DEFAULT]: <Icons.SunOutlined />,
|
||||
[ThemeAlgorithm.DARK]: <Icons.MoonOutlined />,
|
||||
[ThemeMode.SYSTEM]: <Icons.FormatPainterOutlined />,
|
||||
[ThemeMode.COMPACT]: <Icons.CompressOutlined />,
|
||||
[ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -49,9 +49,9 @@ const ThemeSelect: React.FC<ThemeSelectProps> = ({
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: ThemeMode.LIGHT,
|
||||
key: ThemeMode.DEFAULT,
|
||||
label: t('Light'),
|
||||
onClick: () => handleSelect(ThemeMode.LIGHT),
|
||||
onClick: () => handleSelect(ThemeMode.DEFAULT),
|
||||
icon: <Icons.SunOutlined />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { theme as antdThemeImport } from 'antd';
|
||||
import { Theme } from './Theme';
|
||||
import { AnyThemeConfig } from './types';
|
||||
import { AnyThemeConfig, ThemeAlgorithm } from './types';
|
||||
|
||||
// Mock emotion's cache to avoid actual DOM operations
|
||||
jest.mock('@emotion/cache', () => ({
|
||||
@@ -44,7 +44,7 @@ describe('Theme', () => {
|
||||
const parsedJson = JSON.parse(jsonString);
|
||||
|
||||
expect(parsedJson.token?.colorPrimary).toBe('#ff0000');
|
||||
expect(parsedJson.algorithm).toBe('dark');
|
||||
expect(parsedJson.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('Theme', () => {
|
||||
|
||||
// Verify dark mode by using the serialized config from the public method
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe('dark');
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ describe('Theme', () => {
|
||||
|
||||
// Verify the algorithm was updated
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe('dark');
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('Theme', () => {
|
||||
|
||||
// Verify dark algorithm is used
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe('dark');
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
|
||||
it('switches to default algorithm when toggling dark mode off', () => {
|
||||
@@ -164,7 +164,7 @@ describe('Theme', () => {
|
||||
|
||||
// Verify default algorithm is used
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe('default');
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DEFAULT);
|
||||
});
|
||||
|
||||
it('preserves other algorithms when toggling dark mode', () => {
|
||||
@@ -181,10 +181,11 @@ describe('Theme', () => {
|
||||
|
||||
// Verify default algorithm replaces dark but compact is preserved
|
||||
const serialized = theme.toSerializedConfig();
|
||||
|
||||
expect(Array.isArray(serialized.algorithm)).toBe(true);
|
||||
expect(serialized.algorithm).toContain('default');
|
||||
expect(serialized.algorithm).toContain('compact');
|
||||
expect(serialized.algorithm).not.toContain('dark');
|
||||
expect(serialized.algorithm).toContain(ThemeAlgorithm.DEFAULT);
|
||||
expect(serialized.algorithm).toContain(ThemeAlgorithm.COMPACT);
|
||||
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.DARK);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -218,7 +219,7 @@ describe('Theme', () => {
|
||||
const serialized = theme.toSerializedConfig();
|
||||
|
||||
expect(serialized.token?.colorPrimary).toBe('#ff0000');
|
||||
expect(serialized.algorithm).toBe('dark');
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable theme-colors/no-literal-colors */
|
||||
import { SerializableThemeConfig } from './types';
|
||||
import { type SerializableThemeConfig, ThemeAlgorithm } from './types';
|
||||
|
||||
const exampleThemes: Record<string, SerializableThemeConfig> = {
|
||||
superset: {
|
||||
@@ -27,11 +27,11 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
|
||||
},
|
||||
supersetDark: {
|
||||
token: {},
|
||||
algorithm: 'dark',
|
||||
algorithm: ThemeAlgorithm.DARK,
|
||||
},
|
||||
supersetCompact: {
|
||||
token: {},
|
||||
algorithm: 'compact',
|
||||
algorithm: ThemeAlgorithm.COMPACT,
|
||||
},
|
||||
funky: {
|
||||
token: {
|
||||
@@ -43,7 +43,7 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
|
||||
borderRadius: 12,
|
||||
fontFamily: 'Comic Sans MS, cursive',
|
||||
},
|
||||
algorithm: 'default',
|
||||
algorithm: ThemeAlgorithm.DEFAULT,
|
||||
},
|
||||
funkyDark: {
|
||||
token: {
|
||||
@@ -55,7 +55,7 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
|
||||
borderRadius: 12,
|
||||
fontFamily: 'Comic Sans MS, cursive',
|
||||
},
|
||||
algorithm: 'dark',
|
||||
algorithm: ThemeAlgorithm.DARK,
|
||||
},
|
||||
};
|
||||
export default exampleThemes;
|
||||
|
||||
@@ -16,17 +16,17 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import emotionStyled from '@emotion/styled';
|
||||
import emotionStyled, { CreateStyled } from '@emotion/styled';
|
||||
import { useTheme as useThemeBasic } from '@emotion/react';
|
||||
// import { theme as antdThemeImport } from 'antd';
|
||||
import { Theme } from './Theme';
|
||||
import type {
|
||||
SupersetTheme,
|
||||
SerializableThemeConfig,
|
||||
AnyThemeConfig,
|
||||
ThemeStorage,
|
||||
ThemeControllerOptions,
|
||||
ThemeContextType,
|
||||
import {
|
||||
type SupersetTheme,
|
||||
type SerializableThemeConfig,
|
||||
type AnyThemeConfig,
|
||||
type ThemeStorage,
|
||||
type ThemeControllerOptions,
|
||||
type ThemeContextType,
|
||||
ThemeAlgorithm,
|
||||
} from './types';
|
||||
|
||||
export {
|
||||
@@ -56,10 +56,12 @@ export function useTheme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
const styled = emotionStyled;
|
||||
const styled: CreateStyled = emotionStyled;
|
||||
|
||||
// launching in in dark mode for now while iterating
|
||||
const themeObject = Theme.fromConfig({ algorithm: 'default' });
|
||||
const themeObject: Theme = Theme.fromConfig({
|
||||
algorithm: ThemeAlgorithm.DEFAULT,
|
||||
});
|
||||
|
||||
const { theme } = themeObject;
|
||||
const supersetTheme = theme;
|
||||
|
||||
@@ -33,6 +33,41 @@ import { Theme } from '.';
|
||||
export type AntdTokens = ReturnType<typeof antdThemeImport.getDesignToken>;
|
||||
export type AntdThemeConfig = ThemeConfig;
|
||||
|
||||
/**
|
||||
* Theme algorithms supported by Antd.
|
||||
* They can be used individually or in combination.
|
||||
* - DEFAULT: Default light theme
|
||||
* - DARK: Dark theme
|
||||
* - COMPACT: Compact theme (smaller spacing)
|
||||
*/
|
||||
export enum ThemeAlgorithm {
|
||||
DEFAULT = 'default',
|
||||
DARK = 'dark',
|
||||
COMPACT = 'compact',
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the current theme mode of the app.
|
||||
* It can be one of the following:
|
||||
* - DEFAULT: Light theme
|
||||
* - DARK: Dark theme
|
||||
* - SYSTEM: System theme (auto-detects based on system settings)
|
||||
*/
|
||||
export enum ThemeMode {
|
||||
DEFAULT = 'default',
|
||||
DARK = 'dark',
|
||||
SYSTEM = 'system',
|
||||
}
|
||||
|
||||
/**
|
||||
* All valid algorithm values that can be used in theme config.
|
||||
*/
|
||||
export type ThemeAlgorithmOption =
|
||||
| ThemeAlgorithm.DEFAULT
|
||||
| ThemeAlgorithm.DARK
|
||||
| ThemeAlgorithm.COMPACT
|
||||
| ThemeAlgorithm[];
|
||||
|
||||
/**
|
||||
* A serializable version of Ant Design's ThemeConfig
|
||||
* Compatible with theme editor exports
|
||||
@@ -40,11 +75,7 @@ export type AntdThemeConfig = ThemeConfig;
|
||||
export type SerializableThemeConfig = {
|
||||
token?: Record<string, any>;
|
||||
components?: Record<string, any>;
|
||||
algorithm?:
|
||||
| 'default'
|
||||
| 'dark'
|
||||
| 'compact'
|
||||
| ('default' | 'dark' | 'compact')[];
|
||||
algorithm?: ThemeAlgorithmOption;
|
||||
hashed?: boolean;
|
||||
inherit?: boolean;
|
||||
};
|
||||
@@ -358,13 +389,6 @@ export type AllowedAntdTokenKeys = Extract<
|
||||
keyof AntdTokens
|
||||
>;
|
||||
|
||||
export enum ThemeMode {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark',
|
||||
SYSTEM = 'system',
|
||||
COMPACT = 'compact',
|
||||
}
|
||||
|
||||
export type SharedAntdTokens = Pick<AntdTokens, AllowedAntdTokenKeys>;
|
||||
|
||||
/** The final shape for our custom theme object, combining old theme + shared antd + superset specifics. */
|
||||
@@ -379,7 +403,7 @@ export interface ThemeStorage {
|
||||
}
|
||||
|
||||
export interface ThemeControllerOptions {
|
||||
themeObject: Theme;
|
||||
themeObject?: Theme;
|
||||
storage?: ThemeStorage;
|
||||
storageKey?: string;
|
||||
modeStorageKey?: string;
|
||||
@@ -393,6 +417,6 @@ export interface ThemeContextType {
|
||||
theme: Theme;
|
||||
themeMode: ThemeMode;
|
||||
setTheme: (config: AnyThemeConfig) => void;
|
||||
changeThemeMode: (newMode: ThemeMode) => void;
|
||||
setThemeMode: (newMode: ThemeMode) => void;
|
||||
resetTheme: () => void;
|
||||
}
|
||||
|
||||
@@ -28,9 +28,10 @@ import {
|
||||
genDeprecatedColorVariations,
|
||||
} from './utils';
|
||||
import {
|
||||
AnyThemeConfig,
|
||||
SerializableThemeConfig,
|
||||
AntdThemeConfig,
|
||||
type AnyThemeConfig,
|
||||
type SerializableThemeConfig,
|
||||
type AntdThemeConfig,
|
||||
ThemeAlgorithm,
|
||||
} from './types';
|
||||
|
||||
// Mock tinycolor2 for consistent testing
|
||||
@@ -50,22 +51,25 @@ describe('Theme utilities', () => {
|
||||
const config: AnyThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
};
|
||||
|
||||
expect(isSerializableConfig(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when algorithm is a string', () => {
|
||||
const config: AnyThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: 'dark',
|
||||
algorithm: ThemeAlgorithm.DARK,
|
||||
};
|
||||
|
||||
expect(isSerializableConfig(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when algorithm is an array of strings', () => {
|
||||
const config: AnyThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: ['dark', 'compact'],
|
||||
algorithm: [ThemeAlgorithm.DARK, ThemeAlgorithm.COMPACT],
|
||||
};
|
||||
|
||||
expect(isSerializableConfig(config)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -74,15 +78,19 @@ describe('Theme utilities', () => {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
};
|
||||
|
||||
expect(isSerializableConfig(config)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when algorithm is an array containing a function', () => {
|
||||
const config: AnyThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
// @ts-ignore
|
||||
algorithm: [antdThemeImport.darkAlgorithm, 'compact'],
|
||||
algorithm: [
|
||||
antdThemeImport.darkAlgorithm,
|
||||
antdThemeImport.compactAlgorithm,
|
||||
],
|
||||
};
|
||||
|
||||
expect(isSerializableConfig(config)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -91,18 +99,22 @@ describe('Theme utilities', () => {
|
||||
it('converts string algorithm to function reference', () => {
|
||||
const config: SerializableThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: 'dark',
|
||||
algorithm: ThemeAlgorithm.DARK,
|
||||
};
|
||||
|
||||
const result = deserializeThemeConfig(config);
|
||||
|
||||
expect(result.algorithm).toBe(antdThemeImport.darkAlgorithm);
|
||||
});
|
||||
|
||||
it('converts array of string algorithms to function references', () => {
|
||||
const config: SerializableThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: ['dark', 'compact'],
|
||||
algorithm: [ThemeAlgorithm.DARK, ThemeAlgorithm.COMPACT],
|
||||
};
|
||||
|
||||
const result = deserializeThemeConfig(config);
|
||||
|
||||
expect(Array.isArray(result.algorithm)).toBe(true);
|
||||
expect(result.algorithm).toContain(antdThemeImport.darkAlgorithm);
|
||||
expect(result.algorithm).toContain(antdThemeImport.compactAlgorithm);
|
||||
@@ -111,10 +123,12 @@ describe('Theme utilities', () => {
|
||||
it('preserves other configuration properties', () => {
|
||||
const config: SerializableThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: 'dark',
|
||||
algorithm: ThemeAlgorithm.DARK,
|
||||
hashed: true,
|
||||
};
|
||||
|
||||
const result = deserializeThemeConfig(config);
|
||||
|
||||
expect(result.token).toEqual({ colorPrimary: '#ff0000' });
|
||||
expect(result.hashed).toBe(true);
|
||||
});
|
||||
@@ -123,25 +137,31 @@ describe('Theme utilities', () => {
|
||||
const config: SerializableThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
};
|
||||
|
||||
const result = deserializeThemeConfig(config);
|
||||
expect(result.algorithm).toBeUndefined();
|
||||
|
||||
expect(result.algorithm).toBe(antdThemeImport.defaultAlgorithm);
|
||||
});
|
||||
|
||||
it('converts default algorithm string to function reference', () => {
|
||||
const config: SerializableThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: 'default',
|
||||
algorithm: ThemeAlgorithm.DEFAULT,
|
||||
};
|
||||
|
||||
const result = deserializeThemeConfig(config);
|
||||
|
||||
expect(result.algorithm).toBe(antdThemeImport.defaultAlgorithm);
|
||||
});
|
||||
|
||||
it('converts compact algorithm string to function reference', () => {
|
||||
const config: SerializableThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: 'compact',
|
||||
algorithm: ThemeAlgorithm.COMPACT,
|
||||
};
|
||||
|
||||
const result = deserializeThemeConfig(config);
|
||||
|
||||
expect(result.algorithm).toBe(antdThemeImport.compactAlgorithm);
|
||||
});
|
||||
});
|
||||
@@ -152,8 +172,10 @@ describe('Theme utilities', () => {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
};
|
||||
|
||||
const result = serializeThemeConfig(config);
|
||||
expect(result.algorithm).toBe('dark');
|
||||
|
||||
expect(result.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
|
||||
it('converts array of function algorithms to strings', () => {
|
||||
@@ -164,10 +186,12 @@ describe('Theme utilities', () => {
|
||||
antdThemeImport.compactAlgorithm,
|
||||
],
|
||||
};
|
||||
|
||||
const result = serializeThemeConfig(config);
|
||||
|
||||
expect(Array.isArray(result.algorithm)).toBe(true);
|
||||
expect(result.algorithm).toContain('dark');
|
||||
expect(result.algorithm).toContain('compact');
|
||||
expect(result.algorithm).toContain(ThemeAlgorithm.DARK);
|
||||
expect(result.algorithm).toContain(ThemeAlgorithm.COMPACT);
|
||||
});
|
||||
|
||||
it('preserves other configuration properties', () => {
|
||||
@@ -176,7 +200,9 @@ describe('Theme utilities', () => {
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
hashed: true,
|
||||
};
|
||||
|
||||
const result = serializeThemeConfig(config);
|
||||
|
||||
expect(result.token).toEqual({ colorPrimary: '#ff0000' });
|
||||
expect(result.hashed).toBe(true);
|
||||
});
|
||||
@@ -185,7 +211,9 @@ describe('Theme utilities', () => {
|
||||
const config: AntdThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
};
|
||||
|
||||
const result = serializeThemeConfig(config);
|
||||
|
||||
expect(result.algorithm).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -196,8 +224,10 @@ describe('Theme utilities', () => {
|
||||
// @ts-ignore
|
||||
algorithm: unknownAlgorithm,
|
||||
};
|
||||
|
||||
const result = serializeThemeConfig(config);
|
||||
expect(result.algorithm).toBe('default');
|
||||
|
||||
expect(result.algorithm).toBe(ThemeAlgorithm.DEFAULT);
|
||||
});
|
||||
|
||||
it('converts default algorithm function to string', () => {
|
||||
@@ -205,8 +235,10 @@ describe('Theme utilities', () => {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
};
|
||||
|
||||
const result = serializeThemeConfig(config);
|
||||
expect(result.algorithm).toBe('default');
|
||||
|
||||
expect(result.algorithm).toBe(ThemeAlgorithm.DEFAULT);
|
||||
});
|
||||
|
||||
it('converts compact algorithm function to string', () => {
|
||||
@@ -214,8 +246,10 @@ describe('Theme utilities', () => {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: antdThemeImport.compactAlgorithm,
|
||||
};
|
||||
|
||||
const result = serializeThemeConfig(config);
|
||||
expect(result.algorithm).toBe('compact');
|
||||
|
||||
expect(result.algorithm).toBe(ThemeAlgorithm.COMPACT);
|
||||
});
|
||||
|
||||
it('defaults each unknown algorithm in array to "default"', () => {
|
||||
@@ -225,9 +259,14 @@ describe('Theme utilities', () => {
|
||||
// @ts-ignore
|
||||
algorithm: [antdThemeImport.darkAlgorithm, unknownAlgorithm],
|
||||
};
|
||||
|
||||
const result = serializeThemeConfig(config);
|
||||
|
||||
expect(Array.isArray(result.algorithm)).toBe(true);
|
||||
expect(result.algorithm).toEqual(['dark', 'default']);
|
||||
expect(result.algorithm).toEqual([
|
||||
ThemeAlgorithm.DARK,
|
||||
ThemeAlgorithm.DEFAULT,
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles mixed known and unknown algorithms in array', () => {
|
||||
@@ -244,13 +283,15 @@ describe('Theme utilities', () => {
|
||||
unknownAlgorithm2,
|
||||
],
|
||||
};
|
||||
|
||||
const result = serializeThemeConfig(config);
|
||||
|
||||
expect(Array.isArray(result.algorithm)).toBe(true);
|
||||
expect(result.algorithm).toEqual([
|
||||
'dark',
|
||||
'default',
|
||||
'compact',
|
||||
'default',
|
||||
ThemeAlgorithm.DARK,
|
||||
ThemeAlgorithm.DEFAULT,
|
||||
ThemeAlgorithm.COMPACT,
|
||||
ThemeAlgorithm.DEFAULT,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -261,16 +302,20 @@ describe('Theme utilities', () => {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
};
|
||||
|
||||
const result = normalizeThemeConfig(config);
|
||||
|
||||
expect(result).toBe(config);
|
||||
});
|
||||
|
||||
it('deserializes serializable configs', () => {
|
||||
const config: SerializableThemeConfig = {
|
||||
token: { colorPrimary: '#ff0000' },
|
||||
algorithm: 'dark',
|
||||
algorithm: ThemeAlgorithm.DARK,
|
||||
};
|
||||
|
||||
const result = normalizeThemeConfig(config);
|
||||
|
||||
expect(result.algorithm).toBe(antdThemeImport.darkAlgorithm);
|
||||
});
|
||||
});
|
||||
@@ -278,14 +323,18 @@ describe('Theme utilities', () => {
|
||||
describe('getAntdConfig', () => {
|
||||
it('returns config with default algorithm for light mode', () => {
|
||||
const seed = { colorPrimary: '#ff0000' };
|
||||
|
||||
const result = getAntdConfig(seed, false);
|
||||
|
||||
expect(result.token).toBe(seed);
|
||||
expect(result.algorithm).toBe(antdThemeImport.defaultAlgorithm);
|
||||
});
|
||||
|
||||
it('returns config with dark algorithm for dark mode', () => {
|
||||
const seed = { colorPrimary: '#ff0000' };
|
||||
|
||||
const result = getAntdConfig(seed, true);
|
||||
|
||||
expect(result.token).toBe(seed);
|
||||
expect(result.algorithm).toBe(antdThemeImport.darkAlgorithm);
|
||||
});
|
||||
@@ -301,7 +350,9 @@ describe('Theme utilities', () => {
|
||||
colorInfo: '#info',
|
||||
otherToken: 'ignore-me',
|
||||
};
|
||||
|
||||
const result = getSystemColors(tokens);
|
||||
|
||||
expect(result).toEqual({
|
||||
colorPrimary: '#primary',
|
||||
colorError: '#error',
|
||||
@@ -315,6 +366,7 @@ describe('Theme utilities', () => {
|
||||
describe('genDeprecatedColorVariations', () => {
|
||||
it('generates color variations for light mode', () => {
|
||||
const result = genDeprecatedColorVariations('#base-color', false);
|
||||
|
||||
expect(result.base).toBe('#base-color');
|
||||
expect(result.light1).toBe('#mixed-color');
|
||||
expect(result.dark1).toBe('#mixed-color');
|
||||
@@ -322,6 +374,7 @@ describe('Theme utilities', () => {
|
||||
|
||||
it('generates color variations for dark mode', () => {
|
||||
const result = genDeprecatedColorVariations('#base-color', true);
|
||||
|
||||
expect(result.base).toBe('#base-color');
|
||||
expect(result.light1).toBe('#mixed-color');
|
||||
expect(result.dark1).toBe('#mixed-color');
|
||||
@@ -337,7 +390,9 @@ describe('Theme utilities', () => {
|
||||
colorSuccess: '#success',
|
||||
colorInfo: '#info',
|
||||
};
|
||||
|
||||
const result = getDeprecatedColors(systemColors, false);
|
||||
|
||||
expect(result.primary.base).toBe('#primary');
|
||||
expect(result.error.base).toBe('#error');
|
||||
expect(result.warning.base).toBe('#warning');
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
import { theme as antdThemeImport } from 'antd';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import {
|
||||
AntdThemeConfig,
|
||||
AnyThemeConfig,
|
||||
SerializableThemeConfig,
|
||||
DeprecatedColorVariations,
|
||||
DeprecatedThemeColors,
|
||||
SystemColors,
|
||||
SupersetTheme,
|
||||
type AntdThemeConfig,
|
||||
type AnyThemeConfig,
|
||||
type SerializableThemeConfig,
|
||||
type DeprecatedColorVariations,
|
||||
type DeprecatedThemeColors,
|
||||
type SystemColors,
|
||||
type SupersetTheme,
|
||||
ThemeAlgorithm,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
@@ -38,9 +39,8 @@ export function isSerializableConfig(
|
||||
|
||||
if (algorithm === undefined) return true;
|
||||
|
||||
if (Array.isArray(algorithm)) {
|
||||
if (Array.isArray(algorithm))
|
||||
return (algorithm as unknown[]).every(alg => typeof alg === 'string');
|
||||
}
|
||||
|
||||
return typeof algorithm === 'string';
|
||||
}
|
||||
@@ -60,9 +60,21 @@ export function deserializeThemeConfig(
|
||||
|
||||
let resolvedAlgorithm;
|
||||
if (Array.isArray(algorithm)) {
|
||||
resolvedAlgorithm = algorithm.map(alg => algorithmMap[alg]);
|
||||
} else if (algorithm) {
|
||||
const validAlgorithms = algorithm
|
||||
.map((alg: ThemeAlgorithm) => algorithmMap[alg])
|
||||
.filter(Boolean);
|
||||
|
||||
// If we have valid algorithms, use them; otherwise fallback to default
|
||||
if (validAlgorithms.length > 0) {
|
||||
resolvedAlgorithm =
|
||||
validAlgorithms.length === 1 ? validAlgorithms[0] : validAlgorithms;
|
||||
} else {
|
||||
resolvedAlgorithm = antdThemeImport.defaultAlgorithm;
|
||||
}
|
||||
} else if (algorithm && algorithmMap[algorithm]) {
|
||||
resolvedAlgorithm = algorithmMap[algorithm];
|
||||
} else {
|
||||
resolvedAlgorithm = antdThemeImport.defaultAlgorithm;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -83,19 +95,21 @@ export function serializeThemeConfig(
|
||||
|
||||
if (Array.isArray(algorithm)) {
|
||||
serializedAlgorithm = algorithm.map(alg => {
|
||||
if (alg === antdThemeImport.defaultAlgorithm) return 'default';
|
||||
if (alg === antdThemeImport.darkAlgorithm) return 'dark';
|
||||
if (alg === antdThemeImport.compactAlgorithm) return 'compact';
|
||||
return 'default'; // Fallback
|
||||
}) as ('default' | 'dark' | 'compact')[];
|
||||
if (alg === antdThemeImport.defaultAlgorithm)
|
||||
return ThemeAlgorithm.DEFAULT;
|
||||
if (alg === antdThemeImport.darkAlgorithm) return ThemeAlgorithm.DARK;
|
||||
if (alg === antdThemeImport.compactAlgorithm)
|
||||
return ThemeAlgorithm.COMPACT;
|
||||
return ThemeAlgorithm.DEFAULT; // Fallback
|
||||
}) as ThemeAlgorithm[];
|
||||
} else if (algorithm) {
|
||||
if (algorithm === antdThemeImport.defaultAlgorithm)
|
||||
serializedAlgorithm = 'default';
|
||||
serializedAlgorithm = ThemeAlgorithm.DEFAULT;
|
||||
else if (algorithm === antdThemeImport.darkAlgorithm)
|
||||
serializedAlgorithm = 'dark';
|
||||
serializedAlgorithm = ThemeAlgorithm.DARK;
|
||||
else if (algorithm === antdThemeImport.compactAlgorithm)
|
||||
serializedAlgorithm = 'compact';
|
||||
else serializedAlgorithm = 'default'; // Fallback
|
||||
serializedAlgorithm = ThemeAlgorithm.COMPACT;
|
||||
else serializedAlgorithm = ThemeAlgorithm.DEFAULT; // Fallback
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -109,9 +123,8 @@ export function serializeThemeConfig(
|
||||
* This automatically detects and converts serializable configs
|
||||
*/
|
||||
export function normalizeThemeConfig(config: AnyThemeConfig): AntdThemeConfig {
|
||||
if (isSerializableConfig(config)) {
|
||||
return deserializeThemeConfig(config);
|
||||
}
|
||||
if (isSerializableConfig(config)) return deserializeThemeConfig(config);
|
||||
|
||||
return config as AntdThemeConfig;
|
||||
}
|
||||
|
||||
@@ -164,7 +177,7 @@ export function getDeprecatedColors(
|
||||
systemColors: SystemColors,
|
||||
isDark: boolean,
|
||||
): DeprecatedThemeColors {
|
||||
const sc = systemColors;
|
||||
const sc: SystemColors = systemColors;
|
||||
return {
|
||||
primary: genDeprecatedColorVariations(sc.colorPrimary, isDark),
|
||||
error: genDeprecatedColorVariations(sc.colorError, isDark),
|
||||
|
||||
@@ -86,20 +86,33 @@ export default class Translator {
|
||||
}
|
||||
|
||||
translate(input: string, ...args: unknown[]): string {
|
||||
return this.i18n.translate(input).fetch(...args);
|
||||
try {
|
||||
return this.i18n.translate(input).fetch(...args);
|
||||
} catch (err) {
|
||||
logging.warn(`Translation failed for key "${input}" with args:`, args);
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
translateWithNumber(key: string, ...args: unknown[]): string {
|
||||
const [plural, num, ...rest] = args;
|
||||
if (typeof plural === 'number') {
|
||||
try {
|
||||
const [plural, num, ...rest] = args;
|
||||
if (typeof plural === 'number') {
|
||||
return this.i18n
|
||||
.translate(key)
|
||||
.ifPlural(plural, key)
|
||||
.fetch(plural, num, ...rest);
|
||||
}
|
||||
return this.i18n
|
||||
.translate(key)
|
||||
.ifPlural(plural, key)
|
||||
.fetch(plural, num, ...args);
|
||||
.ifPlural(num as number, plural as string)
|
||||
.fetch(...rest);
|
||||
} catch (err) {
|
||||
logging.warn(
|
||||
`Plural translation failed for key "${key}" with args:`,
|
||||
args,
|
||||
);
|
||||
}
|
||||
return this.i18n
|
||||
.translate(key)
|
||||
.ifPlural(num as number, plural as string)
|
||||
.fetch(...rest);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export enum FeatureFlag {
|
||||
EnableTemplateProcessing = 'ENABLE_TEMPLATE_PROCESSING',
|
||||
EscapeMarkdownHtml = 'ESCAPE_MARKDOWN_HTML',
|
||||
EstimateQueryCost = 'ESTIMATE_QUERY_COST',
|
||||
FilterBarClosedByDefault = 'FILTERBAR_CLOSED_BY_DEFAULT',
|
||||
GlobalAsyncQueries = 'GLOBAL_ASYNC_QUERIES',
|
||||
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
|
||||
ScheduledQueries = 'SCHEDULED_QUERIES',
|
||||
|
||||
@@ -19,28 +19,18 @@
|
||||
|
||||
import { t } from '../translation';
|
||||
|
||||
const VALIDE_OSM_URLS = ['https://tile.osm', 'https://tile.openstreetmap'];
|
||||
|
||||
/**
|
||||
* Validate a [Mapbox styles URL](https://docs.mapbox.com/help/glossary/style-url/)
|
||||
* or a title server URL.
|
||||
* @param v
|
||||
*/
|
||||
export default function validateMapboxStylesUrl(v: unknown) {
|
||||
if (typeof v === 'string') {
|
||||
const trimmed_v = v.trim();
|
||||
if (
|
||||
typeof v === 'string' &&
|
||||
trimmed_v.length > 0 &&
|
||||
(trimmed_v.startsWith('mapbox://styles/') ||
|
||||
trimmed_v.startsWith('tile://http') ||
|
||||
VALIDE_OSM_URLS.some(s => trimmed_v.startsWith(s)))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
typeof v === 'string' &&
|
||||
v.trim().length > 0 &&
|
||||
v.trim().startsWith('mapbox://styles/')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return t(
|
||||
'is expected to be a Mapbox/OSM URL (eg. mapbox://styles/...) or a tile server URL (eg. tile://http...)',
|
||||
);
|
||||
return t('is expected to be a Mapbox URL');
|
||||
}
|
||||
|
||||
@@ -29,11 +29,6 @@ describe('validateMapboxStylesUrl', () => {
|
||||
'mapbox://styles/foobar/clp2dr5r4008a01pcg4ad45m8',
|
||||
),
|
||||
).toEqual(false);
|
||||
expect(
|
||||
validateMapboxStylesUrl(
|
||||
'tile://https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
),
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
[
|
||||
@@ -45,7 +40,7 @@ describe('validateMapboxStylesUrl', () => {
|
||||
].forEach(value => {
|
||||
it(`should not validate ${value}`, () => {
|
||||
expect(validateMapboxStylesUrl(value)).toEqual(
|
||||
'is expected to be a Mapbox/OSM URL (eg. mapbox://styles/...) or a tile server URL (eg. tile://http...)',
|
||||
'is expected to be a Mapbox URL',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,11 +24,10 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@deck.gl/aggregation-layers": "^9.1.12",
|
||||
"@deck.gl/core": "^9.1.12",
|
||||
"@deck.gl/geo-layers": "^9.1.12",
|
||||
"@deck.gl/layers": "^9.1.12",
|
||||
"@deck.gl/react": "^9.1.12",
|
||||
"@deck.gl/aggregation-layers": "^9.0.38",
|
||||
"@deck.gl/core": "^9.0.37",
|
||||
"@deck.gl/layers": "^9.0.38",
|
||||
"@deck.gl/react": "^9.1.4",
|
||||
"@mapbox/geojson-extent": "^1.0.1",
|
||||
"@math.gl/web-mercator": "^4.1.0",
|
||||
"@types/d3-array": "^2.0.0",
|
||||
|
||||
@@ -44,9 +44,12 @@ import {
|
||||
DeckGLContainerHandle,
|
||||
DeckGLContainerStyledWrapper,
|
||||
} from './DeckGLContainer';
|
||||
import { Point } from './types';
|
||||
import { GetLayerType } from './factory';
|
||||
import { ColorBreakpointType, ColorType, Point } from './types';
|
||||
import { TooltipProps } from './components/Tooltip';
|
||||
import { COLOR_SCHEME_TYPES, ColorSchemeType } from './utilities/utils';
|
||||
import { getColorBreakpointsBuckets } from './utils';
|
||||
import { DEFAULT_DECKGL_COLOR } from './utilities/Shared_DeckGL';
|
||||
|
||||
const { getScale } = CategoricalColorNamespace;
|
||||
|
||||
@@ -55,18 +58,24 @@ function getCategories(fd: QueryFormData, data: JsonObject[]) {
|
||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorFn = getScale(appliedScheme);
|
||||
const categories: Record<any, { color: any; enabled: boolean }> = {};
|
||||
data.forEach(d => {
|
||||
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
|
||||
} else {
|
||||
color = fixedColor;
|
||||
let categories: Record<any, { color: any; enabled: boolean }> = {};
|
||||
|
||||
const colorSchemeType = fd.color_scheme_type;
|
||||
if (colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints) {
|
||||
categories = getColorBreakpointsBuckets(fd.color_breakpoints);
|
||||
} else {
|
||||
data.forEach(d => {
|
||||
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
|
||||
} else {
|
||||
color = fixedColor;
|
||||
}
|
||||
categories[d.cat_color] = { color, enabled: true };
|
||||
}
|
||||
categories[d.cat_color] = { color, enabled: true };
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
@@ -133,22 +142,73 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const addColor = useCallback((data: JsonObject[], fd: QueryFormData) => {
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorFn = getScale(appliedScheme);
|
||||
const addColor = useCallback(
|
||||
(
|
||||
data: JsonObject[],
|
||||
fd: QueryFormData,
|
||||
selectedColorScheme: ColorSchemeType,
|
||||
) => {
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorFn = getScale(appliedScheme);
|
||||
let color: ColorType;
|
||||
|
||||
return data.map(d => {
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
color = hexToRGB(colorFn(d.cat_color, fd.slice_id), c.a * 255);
|
||||
switch (selectedColorScheme) {
|
||||
case COLOR_SCHEME_TYPES.fixed_color: {
|
||||
color = fd.color_picker || { r: 0, g: 0, b: 0, a: 100 };
|
||||
|
||||
return { ...d, color };
|
||||
return data.map(d => ({
|
||||
...d,
|
||||
color: [color.r, color.g, color.b, color.a * 255],
|
||||
}));
|
||||
}
|
||||
case COLOR_SCHEME_TYPES.categorical_palette: {
|
||||
return data.map(d => ({
|
||||
...d,
|
||||
color: hexToRGB(colorFn(d.cat_color, fd.slice_id)),
|
||||
}));
|
||||
}
|
||||
case COLOR_SCHEME_TYPES.color_breakpoints: {
|
||||
const defaultBreakpointColor = fd.deafult_breakpoint_color
|
||||
? [
|
||||
fd.deafult_breakpoint_color.r,
|
||||
fd.deafult_breakpoint_color.g,
|
||||
fd.deafult_breakpoint_color.b,
|
||||
fd.deafult_breakpoint_color.a * 255,
|
||||
]
|
||||
: [
|
||||
DEFAULT_DECKGL_COLOR.r,
|
||||
DEFAULT_DECKGL_COLOR.g,
|
||||
DEFAULT_DECKGL_COLOR.b,
|
||||
DEFAULT_DECKGL_COLOR.a * 255,
|
||||
];
|
||||
return data.map(d => {
|
||||
const breakpointForPoint: ColorBreakpointType =
|
||||
fd.color_breakpoints?.find(
|
||||
(breakpoint: ColorBreakpointType) =>
|
||||
d.metric >= breakpoint.minValue &&
|
||||
d.metric <= breakpoint.maxValue,
|
||||
);
|
||||
|
||||
return {
|
||||
...d,
|
||||
color: breakpointForPoint
|
||||
? [
|
||||
breakpointForPoint?.color.r,
|
||||
breakpointForPoint?.color.g,
|
||||
breakpointForPoint?.color.b,
|
||||
breakpointForPoint?.color.a * 255,
|
||||
]
|
||||
: defaultBreakpointColor,
|
||||
};
|
||||
});
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return d;
|
||||
});
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const getLayers = useCallback(() => {
|
||||
const {
|
||||
@@ -163,8 +223,10 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
} = props;
|
||||
let features = payload.data.features ? [...payload.data.features] : [];
|
||||
|
||||
const selectedColorScheme = fd.color_scheme_type;
|
||||
|
||||
// Add colors from categories or fixed color
|
||||
features = addColor(features, fd);
|
||||
features = addColor(features, fd, selectedColorScheme);
|
||||
|
||||
// Apply user defined data mutator if defined
|
||||
if (fd.js_data_mutator) {
|
||||
|
||||
@@ -39,12 +39,6 @@ import { JsonObject, JsonValue, styled, usePrevious } from '@superset-ui/core';
|
||||
import Tooltip, { TooltipProps } from './components/Tooltip';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { Viewport } from './utils/fitViewport';
|
||||
import {
|
||||
MAPBOX_LAYER_PREFIX,
|
||||
OSM_LAYER_KEYWORDS,
|
||||
TILE_LAYER_PREFIX,
|
||||
buildTileLayer,
|
||||
} from './utils';
|
||||
|
||||
const TICK = 250; // milliseconds
|
||||
|
||||
@@ -108,20 +102,6 @@ export const DeckGLContainer = memo(
|
||||
);
|
||||
|
||||
const layers = useCallback(() => {
|
||||
if (
|
||||
(props.mapStyle?.startsWith(TILE_LAYER_PREFIX) ||
|
||||
OSM_LAYER_KEYWORDS.some(tilek => props.mapStyle?.includes(tilek))) &&
|
||||
props.layers.some(
|
||||
l => typeof l !== 'function' && l?.id === 'tile-layer',
|
||||
) === false
|
||||
) {
|
||||
props.layers.unshift(
|
||||
buildTileLayer(
|
||||
(props.mapStyle ?? '').replace(TILE_LAYER_PREFIX, ''),
|
||||
'tile-layer',
|
||||
),
|
||||
);
|
||||
}
|
||||
// Support for layer factory
|
||||
if (props.layers.some(l => typeof l === 'function')) {
|
||||
return props.layers.map(l =>
|
||||
@@ -130,7 +110,7 @@ export const DeckGLContainer = memo(
|
||||
}
|
||||
|
||||
return props.layers as Layer[];
|
||||
}, [props.layers, props.mapStyle]);
|
||||
}, [props.layers]);
|
||||
|
||||
const { children = null, height, width } = props;
|
||||
|
||||
@@ -154,13 +134,11 @@ export const DeckGLContainer = memo(
|
||||
glContextRef.current = context.gl;
|
||||
}}
|
||||
>
|
||||
{props.mapStyle?.startsWith(MAPBOX_LAYER_PREFIX) && (
|
||||
<StaticMap
|
||||
preserveDrawingBuffer
|
||||
mapStyle={props.mapStyle || 'light'}
|
||||
mapboxApiAccessToken={props.mapboxApiAccessToken}
|
||||
/>
|
||||
)}
|
||||
<StaticMap
|
||||
preserveDrawingBuffer
|
||||
mapStyle={props.mapStyle || 'light'}
|
||||
mapboxApiAccessToken={props.mapboxApiAccessToken}
|
||||
/>
|
||||
</DeckGL>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
import { memo } from 'react';
|
||||
import { formatNumber, styled } from '@superset-ui/core';
|
||||
import { Color } from '@deck.gl/core';
|
||||
|
||||
const StyledLegend = styled.div`
|
||||
${({ theme }) => `
|
||||
@@ -59,7 +60,7 @@ export type LegendProps = {
|
||||
format: string | null;
|
||||
forceCategorical?: boolean;
|
||||
position?: null | 'tl' | 'tr' | 'bl' | 'br';
|
||||
categories: Record<string, { enabled: boolean; color: number[] | undefined }>;
|
||||
categories: Record<string, { enabled: boolean; color: Color | undefined }>;
|
||||
toggleCategory?: (key: string) => void;
|
||||
showSingleCategory?: (key: string) => void;
|
||||
};
|
||||
|
||||
@@ -40,6 +40,8 @@ import CategoricalDeckGLContainer from './CategoricalDeckGLContainer';
|
||||
import fitViewport, { Viewport } from './utils/fitViewport';
|
||||
import { Point } from './types';
|
||||
import { TooltipProps } from './components/Tooltip';
|
||||
import { getColorBreakpointsBuckets } from './utils';
|
||||
import Legend from './components/Legend';
|
||||
|
||||
type DeckGLComponentProps = {
|
||||
datasource: Datasource;
|
||||
@@ -103,6 +105,9 @@ export function createDeckGLComponent(
|
||||
}
|
||||
return props.viewport;
|
||||
};
|
||||
const [categories, setCategories] = useState<JsonObject>(
|
||||
getColorBreakpointsBuckets(props.formData.color_breakpoints) || [],
|
||||
);
|
||||
|
||||
const [viewport, setViewport] = useState(getAdjustedViewport());
|
||||
|
||||
@@ -139,6 +144,14 @@ export function createDeckGLComponent(
|
||||
[setTooltip],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const categories = getColorBreakpointsBuckets(
|
||||
props.formData.color_breakpoints,
|
||||
);
|
||||
|
||||
setCategories(categories);
|
||||
}, [props]);
|
||||
|
||||
const [layer, setLayer] = useState(computeLayer(props));
|
||||
|
||||
useEffect(() => {
|
||||
@@ -161,17 +174,25 @@ export function createDeckGLComponent(
|
||||
const { formData, payload, setControlValue, height, width } = props;
|
||||
|
||||
return (
|
||||
<DeckGLContainerStyledWrapper
|
||||
ref={containerRef}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
width={width}
|
||||
height={height}
|
||||
onViewportChange={setViewport}
|
||||
/>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<DeckGLContainerStyledWrapper
|
||||
ref={containerRef}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
width={width}
|
||||
height={height}
|
||||
onViewportChange={setViewport}
|
||||
/>
|
||||
<Legend
|
||||
forceCategorical
|
||||
categories={categories}
|
||||
format={props.formData.legend_format}
|
||||
position={props.formData.legend_position}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
import { ArcLayer } from '@deck.gl/layers';
|
||||
import { JsonObject, QueryFormData, t } from '@superset-ui/core';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
import { commonLayerProps } from '../common';
|
||||
import { GetLayerType, createCategoricalDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
@@ -68,12 +69,24 @@ export const getLayer: GetLayerType<ArcLayer> = function ({
|
||||
const sc = fd.color_picker;
|
||||
const tc = fd.target_color_picker;
|
||||
|
||||
const colorSchemeType = fd.color_scheme_type;
|
||||
|
||||
return new ArcLayer({
|
||||
data,
|
||||
getSourceColor: (d: any) =>
|
||||
d.sourceColor || d.color || [sc.r, sc.g, sc.b, 255 * sc.a],
|
||||
getTargetColor: (d: any) =>
|
||||
d.targetColor || d.color || [tc.r, tc.g, tc.b, 255 * tc.a],
|
||||
getSourceColor: (d: any) => {
|
||||
if (colorSchemeType === COLOR_SCHEME_TYPES.fixed_color) {
|
||||
return [sc.r, sc.g, sc.b, 255 * sc.a];
|
||||
}
|
||||
|
||||
return d.targetColor || d.color;
|
||||
},
|
||||
getTargetColor: (d: any) => {
|
||||
if (colorSchemeType === COLOR_SCHEME_TYPES.fixed_color) {
|
||||
return [tc.r, tc.g, tc.b, 255 * tc.a];
|
||||
}
|
||||
|
||||
return d.targetColor || d.color;
|
||||
},
|
||||
id: `path-layer-${fd.slice_id}` as const,
|
||||
getWidth: fd.stroke_width ? fd.stroke_width : 3,
|
||||
...commonLayerProps({
|
||||
|
||||
@@ -22,11 +22,14 @@ import timeGrainSqlaAnimationOverrides, {
|
||||
columnChoices,
|
||||
PRIMARY_COLOR,
|
||||
} from '../../utilities/controls';
|
||||
import { formatSelectOptions } from '../../utilities/utils';
|
||||
import {
|
||||
COLOR_SCHEME_TYPES,
|
||||
formatSelectOptions,
|
||||
isColorSchemeTypeVisible,
|
||||
} from '../../utilities/utils';
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
dimension,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
@@ -35,6 +38,9 @@ import {
|
||||
legendPosition,
|
||||
viewport,
|
||||
mapboxStyle,
|
||||
deckGLCategoricalColor,
|
||||
deckGLCategoricalColorSchemeSelect,
|
||||
deckGLCategoricalColorSchemeTypeSelect,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -81,7 +87,37 @@ const config: ControlPanelConfig = {
|
||||
label: t('Arc'),
|
||||
controlSetRows: [
|
||||
[
|
||||
'color_picker',
|
||||
{
|
||||
name: 'color_scheme_type',
|
||||
config: {
|
||||
...deckGLCategoricalColorSchemeTypeSelect.config,
|
||||
choices: [
|
||||
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
|
||||
[
|
||||
COLOR_SCHEME_TYPES.categorical_palette,
|
||||
t('Categorical palette'),
|
||||
],
|
||||
],
|
||||
default: COLOR_SCHEME_TYPES.fixed_color,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'color_picker',
|
||||
config: {
|
||||
label: t('Source Color'),
|
||||
description: t('Color of the source location'),
|
||||
type: 'ColorPickerControl',
|
||||
default: PRIMARY_COLOR,
|
||||
renderTrigger: true,
|
||||
visibility: ({ controls }) =>
|
||||
isColorSchemeTypeVisible(
|
||||
controls,
|
||||
COLOR_SCHEME_TYPES.fixed_color,
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'target_color_picker',
|
||||
config: {
|
||||
@@ -90,22 +126,16 @@ const config: ControlPanelConfig = {
|
||||
type: 'ColorPickerControl',
|
||||
default: PRIMARY_COLOR,
|
||||
renderTrigger: true,
|
||||
visibility: ({ controls }) =>
|
||||
isColorSchemeTypeVisible(
|
||||
controls,
|
||||
COLOR_SCHEME_TYPES.fixed_color,
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: dimension.name,
|
||||
config: {
|
||||
...dimension.config,
|
||||
label: t('Categorical Color'),
|
||||
description: t(
|
||||
'Pick a dimension from which categorical colors are defined',
|
||||
),
|
||||
},
|
||||
},
|
||||
'color_scheme',
|
||||
],
|
||||
[deckGLCategoricalColor],
|
||||
[deckGLCategoricalColorSchemeSelect],
|
||||
[
|
||||
{
|
||||
name: 'stroke_width',
|
||||
@@ -119,9 +149,9 @@ const config: ControlPanelConfig = {
|
||||
choices: formatSelectOptions([1, 2, 3, 4, 5]),
|
||||
},
|
||||
},
|
||||
legendPosition,
|
||||
],
|
||||
[legendFormat, null],
|
||||
[legendPosition],
|
||||
[legendFormat],
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,15 +16,19 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Color } from '@deck.gl/core';
|
||||
import { GridLayer } from '@deck.gl/aggregation-layers';
|
||||
import { t, CategoricalColorNamespace, JsonObject } from '@superset-ui/core';
|
||||
|
||||
import { commonLayerProps, getAggFunc } from '../common';
|
||||
import {
|
||||
commonLayerProps,
|
||||
getAggFunc,
|
||||
getColorForBreakpoints,
|
||||
getColorRange,
|
||||
} from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { hexToRGB } from '../../utils/colors';
|
||||
import { createDeckGLComponent, GetLayerType } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
return (
|
||||
@@ -55,9 +59,6 @@ export const getLayer: GetLayerType<GridLayer> = function ({
|
||||
const fd = formData;
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
const colorRange = colorScale
|
||||
.range()
|
||||
.map(color => hexToRGB(color)) as Color[];
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
@@ -66,19 +67,39 @@ export const getLayer: GetLayerType<GridLayer> = function ({
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
const colorBreakpoints = fd.color_breakpoints;
|
||||
|
||||
const colorSchemeType = fd.color_scheme_type;
|
||||
const colorRange = getColorRange({
|
||||
defaultBreakpointsColor: fd.deafult_breakpoint_color,
|
||||
colorSchemeType,
|
||||
colorScale,
|
||||
colorBreakpoints,
|
||||
fixedColor: fd.color_picker,
|
||||
});
|
||||
|
||||
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
|
||||
|
||||
const colorAggFunc =
|
||||
colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints
|
||||
? (p: number[]) => getColorForBreakpoints(aggFunc, p, colorBreakpoints)
|
||||
: aggFunc;
|
||||
|
||||
return new GridLayer({
|
||||
id: `grid-layer-${fd.slice_id}` as const,
|
||||
id: `grid-layer-${fd.slice_id}-${JSON.stringify(colorBreakpoints)}` as const,
|
||||
data,
|
||||
cellSize: fd.grid_size,
|
||||
extruded: fd.extruded,
|
||||
colorDomain:
|
||||
colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints && colorRange
|
||||
? [0, colorRange.length]
|
||||
: undefined,
|
||||
colorRange,
|
||||
outline: false,
|
||||
// @ts-ignore
|
||||
getElevationValue: aggFunc,
|
||||
// @ts-ignore
|
||||
getColorValue: aggFunc,
|
||||
getColorValue: colorAggFunc,
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setDataMask,
|
||||
|
||||
@@ -33,7 +33,10 @@ import {
|
||||
viewport,
|
||||
spatial,
|
||||
mapboxStyle,
|
||||
legendPosition,
|
||||
generateDeckGLColorSchemeControls,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
@@ -53,7 +56,11 @@ const config: ControlPanelConfig = {
|
||||
controlSetRows: [
|
||||
[mapboxStyle],
|
||||
[viewport],
|
||||
['color_scheme'],
|
||||
...generateDeckGLColorSchemeControls({
|
||||
defaultSchemeType: COLOR_SCHEME_TYPES.categorical_palette,
|
||||
disableCategoricalColumn: true,
|
||||
}),
|
||||
[legendPosition],
|
||||
[autozoom],
|
||||
[gridSize],
|
||||
[extruded],
|
||||
|
||||
@@ -17,11 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { HeatmapLayer } from '@deck.gl/aggregation-layers';
|
||||
import { Position, Color } from '@deck.gl/core';
|
||||
import { Position } from '@deck.gl/core';
|
||||
import { t, getSequentialSchemeRegistry, JsonObject } from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
import { commonLayerProps, getColorRange } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { hexToRGB } from '../../utils/colors';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
|
||||
@@ -63,10 +62,15 @@ export const getLayer: GetLayerType<HeatmapLayer> = ({
|
||||
const colorScale = getSequentialSchemeRegistry()
|
||||
?.get(colorScheme)
|
||||
?.createLinearScale([0, 6]);
|
||||
const colorRange = colorScale
|
||||
?.range()
|
||||
?.map(color => hexToRGB(color))
|
||||
?.reverse() as Color[];
|
||||
|
||||
const colorSchemeType = fd.color_scheme_type;
|
||||
const colorRange = getColorRange({
|
||||
defaultBreakpointsColor: fd.deafult_breakpoint_color,
|
||||
colorBreakpoints: fd.color_breakpoints,
|
||||
fixedColor: fd.color_picker,
|
||||
colorSchemeType,
|
||||
colorScale,
|
||||
})?.reverse();
|
||||
|
||||
return new HeatmapLayer({
|
||||
id: `heatmap-layer-${fd.slice_id}` as const,
|
||||
|
||||
@@ -28,6 +28,9 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
autozoom,
|
||||
deckGLCategoricalColorSchemeTypeSelect,
|
||||
deckGLFixedColor,
|
||||
deckGLLinearColorSchemeSelect,
|
||||
filterNulls,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
@@ -37,6 +40,7 @@ import {
|
||||
spatial,
|
||||
viewport,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
|
||||
const INTENSITY_OPTIONS = Array.from(
|
||||
{ length: 10 },
|
||||
@@ -99,7 +103,21 @@ const config: ControlPanelConfig = {
|
||||
controlSetRows: [
|
||||
[mapboxStyle],
|
||||
[viewport],
|
||||
['linear_color_scheme'],
|
||||
[
|
||||
{
|
||||
name: 'color_scheme_type',
|
||||
config: {
|
||||
...deckGLCategoricalColorSchemeTypeSelect.config,
|
||||
choices: [
|
||||
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
|
||||
[COLOR_SCHEME_TYPES.linear_palette, t('Linear palette')],
|
||||
],
|
||||
default: COLOR_SCHEME_TYPES.linear_palette,
|
||||
},
|
||||
},
|
||||
],
|
||||
[deckGLFixedColor],
|
||||
[deckGLLinearColorSchemeSelect],
|
||||
[autozoom],
|
||||
[
|
||||
{
|
||||
|
||||
@@ -16,13 +16,17 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Color } from '@deck.gl/core';
|
||||
import { HexagonLayer } from '@deck.gl/aggregation-layers';
|
||||
import { t, CategoricalColorNamespace, JsonObject } from '@superset-ui/core';
|
||||
|
||||
import { commonLayerProps, getAggFunc } from '../common';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
import {
|
||||
commonLayerProps,
|
||||
getAggFunc,
|
||||
getColorForBreakpoints,
|
||||
getColorRange,
|
||||
} from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { hexToRGB } from '../../utils/colors';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
|
||||
@@ -54,9 +58,6 @@ export const getLayer: GetLayerType<HexagonLayer> = function ({
|
||||
const fd = formData;
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
const colorRange = colorScale
|
||||
.range()
|
||||
.map(color => hexToRGB(color)) as Color[];
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
@@ -64,19 +65,40 @@ export const getLayer: GetLayerType<HexagonLayer> = function ({
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
const aggFunc = getAggFunc(fd.js_agg_function, p => p?.weight);
|
||||
|
||||
const colorSchemeType = fd.color_scheme_type;
|
||||
const colorRange = getColorRange({
|
||||
defaultBreakpointsColor: fd.deafult_breakpoint_color,
|
||||
colorBreakpoints: fd.color_breakpoints,
|
||||
fixedColor: fd.color_picker,
|
||||
colorSchemeType,
|
||||
colorScale,
|
||||
});
|
||||
|
||||
const colorBreakpoints = fd.color_breakpoints;
|
||||
|
||||
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
|
||||
|
||||
const colorAggFunc =
|
||||
colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints
|
||||
? (p: number[]) => getColorForBreakpoints(aggFunc, p, colorBreakpoints)
|
||||
: aggFunc;
|
||||
|
||||
return new HexagonLayer({
|
||||
id: `hex-layer-${fd.slice_id}` as const,
|
||||
id: `hex-layer-${fd.slice_id}-${JSON.stringify(colorBreakpoints)}` as const,
|
||||
data,
|
||||
radius: fd.grid_size,
|
||||
extruded: fd.extruded,
|
||||
colorDomain:
|
||||
colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints && colorRange
|
||||
? [0, colorRange.length]
|
||||
: undefined,
|
||||
colorRange,
|
||||
outline: false,
|
||||
// @ts-ignore
|
||||
getElevationValue: aggFunc,
|
||||
// @ts-ignore
|
||||
getColorValue: aggFunc,
|
||||
getColorValue: colorAggFunc,
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
autozoom,
|
||||
extruded,
|
||||
filterNulls,
|
||||
generateDeckGLColorSchemeControls,
|
||||
gridSize,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
@@ -52,7 +53,8 @@ const config: ControlPanelConfig = {
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
[mapboxStyle],
|
||||
['color_scheme', viewport],
|
||||
...generateDeckGLColorSchemeControls({}),
|
||||
[viewport],
|
||||
[autozoom],
|
||||
[gridSize],
|
||||
[extruded],
|
||||
|
||||
@@ -36,11 +36,16 @@ import {
|
||||
|
||||
import { PolygonLayer } from '@deck.gl/layers';
|
||||
|
||||
import { Color } from '@deck.gl/core';
|
||||
import Legend from '../../components/Legend';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { getBuckets, getBreakPointColorScaler } from '../../utils';
|
||||
import {
|
||||
getBuckets,
|
||||
getBreakPointColorScaler,
|
||||
getColorBreakpointsBuckets,
|
||||
} from '../../utils';
|
||||
|
||||
import { commonLayerProps } from '../common';
|
||||
import { commonLayerProps, getColorForBreakpoints } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import getPointsFromPolygon from '../../utils/getPointsFromPolygon';
|
||||
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||
@@ -50,6 +55,8 @@ import {
|
||||
} from '../../DeckGLContainer';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { GetLayerType } from '../../factory';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
import { DEFAULT_DECKGL_COLOR } from '../../utilities/Shared_DeckGL';
|
||||
|
||||
const DOUBLE_CLICK_THRESHOLD = 250; // milliseconds
|
||||
|
||||
@@ -107,8 +114,11 @@ export const getLayer: GetLayerType<PolygonLayer> = function ({
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData as PolygonFormData;
|
||||
const fc = fd.fill_color_picker;
|
||||
const sc = fd.stroke_color_picker;
|
||||
const fc: { r: number; g: number; b: number; a: number } =
|
||||
fd.fill_color_picker;
|
||||
const sc: { r: number; g: number; b: number; a: number } =
|
||||
fd.stroke_color_picker;
|
||||
const defaultBreakpointColor = fd.deafult_breakpoint_color;
|
||||
let data = [...payload.data.features];
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
@@ -117,17 +127,62 @@ export const getLayer: GetLayerType<PolygonLayer> = function ({
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
const colorSchemeType = fd.color_scheme_type;
|
||||
|
||||
const metricLabel = fd.metric ? fd.metric.label || fd.metric : null;
|
||||
const accessor = (d: JsonObject) => d[metricLabel];
|
||||
// base color for the polygons
|
||||
const baseColorScaler =
|
||||
fd.metric === null
|
||||
? () => [fc.r, fc.g, fc.b, 255 * fc.a]
|
||||
: getBreakPointColorScaler(fd, data, accessor);
|
||||
let baseColorScaler: (d: JsonObject) => Color;
|
||||
|
||||
switch (colorSchemeType) {
|
||||
case COLOR_SCHEME_TYPES.fixed_color: {
|
||||
baseColorScaler = () => [fc.r, fc.g, fc.b, 255 * fc.a];
|
||||
break;
|
||||
}
|
||||
case COLOR_SCHEME_TYPES.linear_palette: {
|
||||
baseColorScaler =
|
||||
fd.metric === null
|
||||
? () => [fc.r, fc.g, fc.b, 255 * fc.a]
|
||||
: getBreakPointColorScaler(fd, data, accessor);
|
||||
break;
|
||||
}
|
||||
case COLOR_SCHEME_TYPES.color_breakpoints: {
|
||||
const colorBreakpoints = fd.color_breakpoints;
|
||||
baseColorScaler = data => {
|
||||
const breakpointIndex = getColorForBreakpoints(
|
||||
accessor,
|
||||
data as number[],
|
||||
colorBreakpoints,
|
||||
);
|
||||
const breakpointColor =
|
||||
breakpointIndex !== undefined &&
|
||||
colorBreakpoints[breakpointIndex - 1]?.color;
|
||||
return breakpointColor
|
||||
? [breakpointColor.r, breakpointColor.g, breakpointColor.b, 255]
|
||||
: defaultBreakpointColor
|
||||
? [
|
||||
defaultBreakpointColor.r,
|
||||
defaultBreakpointColor.g,
|
||||
defaultBreakpointColor.b,
|
||||
defaultBreakpointColor.a * 255,
|
||||
]
|
||||
: [
|
||||
DEFAULT_DECKGL_COLOR.r,
|
||||
DEFAULT_DECKGL_COLOR.g,
|
||||
DEFAULT_DECKGL_COLOR.b,
|
||||
DEFAULT_DECKGL_COLOR.a * 255,
|
||||
];
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
baseColorScaler = () => [fc.r, fc.g, fc.b, 255 * fc.a];
|
||||
break;
|
||||
}
|
||||
|
||||
// when polygons are selected, reduce the opacity of non-selected polygons
|
||||
const colorScaler = (d: JsonObject): [number, number, number, number] => {
|
||||
const baseColor = (baseColorScaler?.(d) as [
|
||||
const baseColor = (baseColorScaler(d) as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
@@ -154,11 +209,11 @@ export const getLayer: GetLayerType<PolygonLayer> = function ({
|
||||
stroked: fd.stroked,
|
||||
getPolygon: getPointsFromPolygon,
|
||||
getFillColor: colorScaler,
|
||||
getLineColor: [sc.r, sc.g, sc.b, 255 * sc.a],
|
||||
getLineColor: sc ? [sc.r, sc.g, sc.b, 255 * sc.a] : undefined,
|
||||
getLineWidth: fd.line_width,
|
||||
extruded: fd.extruded,
|
||||
lineWidthUnits: fd.line_width_unit,
|
||||
getElevation: (d: any) => getElevation(d, colorScaler),
|
||||
getElevation: (d: JsonObject) => getElevation(d, colorScaler),
|
||||
elevationScale: fd.multiplier,
|
||||
fp64: true,
|
||||
...commonLayerProps({
|
||||
@@ -313,7 +368,10 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
: null;
|
||||
const accessor = (d: JsonObject) => d[metricLabel];
|
||||
|
||||
const buckets = getBuckets(formData, payload.data.features, accessor);
|
||||
const colorSchemeType = formData.color_scheme_type;
|
||||
const buckets = colorSchemeType
|
||||
? getColorBreakpointsBuckets(formData.color_breakpoints)
|
||||
: getBuckets(formData, payload.data.features, accessor);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t } from '@superset-ui/core';
|
||||
import timeGrainSqlaAnimationOverrides from '../../utilities/controls';
|
||||
import { formatSelectOptions } from '../../utilities/utils';
|
||||
import { COLOR_SCHEME_TYPES, formatSelectOptions } from '../../utilities/utils';
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
@@ -44,6 +44,10 @@ import {
|
||||
lineType,
|
||||
reverseLongLat,
|
||||
mapboxStyle,
|
||||
deckGLCategoricalColorSchemeTypeSelect,
|
||||
deckGLLinearColorSchemeSelect,
|
||||
deckGLColorBreakpointsSelect,
|
||||
breakpointsDefaultColor,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
import { dndLineColumn } from '../../utilities/sharedDndControls';
|
||||
|
||||
@@ -96,7 +100,25 @@ const config: ControlPanelConfig = {
|
||||
label: t('Polygon Settings'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[fillColorPicker, strokeColorPicker],
|
||||
[
|
||||
{
|
||||
...deckGLCategoricalColorSchemeTypeSelect,
|
||||
config: {
|
||||
...deckGLCategoricalColorSchemeTypeSelect.config,
|
||||
choices: [
|
||||
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
|
||||
[COLOR_SCHEME_TYPES.linear_palette, t('Linear palette')],
|
||||
[COLOR_SCHEME_TYPES.color_breakpoints, t('Color breakpoints')],
|
||||
],
|
||||
default: COLOR_SCHEME_TYPES.linear_palette,
|
||||
},
|
||||
},
|
||||
fillColorPicker,
|
||||
strokeColorPicker,
|
||||
deckGLLinearColorSchemeSelect,
|
||||
breakpointsDefaultColor,
|
||||
deckGLColorBreakpointsSelect,
|
||||
],
|
||||
[filled, stroked],
|
||||
[extruded],
|
||||
[multiplier],
|
||||
@@ -116,7 +138,6 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
['linear_color_scheme'],
|
||||
[
|
||||
{
|
||||
name: 'opacity',
|
||||
|
||||
@@ -22,7 +22,6 @@ import timeGrainSqlaAnimationOverrides from '../../utilities/controls';
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
dimension,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
@@ -34,7 +33,9 @@ import {
|
||||
pointRadiusFixed,
|
||||
multiplier,
|
||||
mapboxStyle,
|
||||
generateDeckGLColorSchemeControls,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
onInit: controlState => ({
|
||||
@@ -127,22 +128,11 @@ const config: ControlPanelConfig = {
|
||||
{
|
||||
label: t('Point Color'),
|
||||
controlSetRows: [
|
||||
['color_picker'],
|
||||
[legendPosition],
|
||||
[legendFormat],
|
||||
[
|
||||
{
|
||||
name: dimension.name,
|
||||
config: {
|
||||
...dimension.config,
|
||||
label: t('Categorical Color'),
|
||||
description: t(
|
||||
'Pick a dimension from which categorical colors are defined',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
['color_scheme'],
|
||||
...generateDeckGLColorSchemeControls({
|
||||
defaultSchemeType: COLOR_SCHEME_TYPES.fixed_color,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
*/
|
||||
|
||||
import { ScreenGridLayer } from '@deck.gl/aggregation-layers';
|
||||
import { JsonObject, t } from '@superset-ui/core';
|
||||
import { CategoricalColorNamespace, JsonObject, t } from '@superset-ui/core';
|
||||
import { Color } from '@deck.gl/core';
|
||||
import { COLOR_SCHEME_TYPES, ColorSchemeType } from '../../utilities/utils';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { commonLayerProps } from '../common';
|
||||
import { commonLayerProps, getColorRange } from '../common';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
|
||||
@@ -41,7 +43,7 @@ function setTooltipContent(o: JsonObject) {
|
||||
<TooltipRow
|
||||
// eslint-disable-next-line prefer-template
|
||||
label={t('Weight') + ': '}
|
||||
value={`${o.object?.cellWeight}`}
|
||||
value={`${o.object?.value}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -57,11 +59,9 @@ export const getLayer: GetLayerType<ScreenGridLayer> = function ({
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const c = fd.color_picker;
|
||||
let data = payload.data.features.map((d: JsonObject) => ({
|
||||
...d,
|
||||
color: [c.r, c.g, c.b, 255 * c.a],
|
||||
}));
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
@@ -69,16 +69,39 @@ export const getLayer: GetLayerType<ScreenGridLayer> = function ({
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
const colorSchemeType = fd.color_scheme_type as ColorSchemeType & 'default';
|
||||
const colorRange = getColorRange({
|
||||
defaultBreakpointsColor: fd.deafult_breakpoint_color,
|
||||
colorBreakpoints: fd.color_breakpoints,
|
||||
fixedColor: fd.color_picker,
|
||||
colorSchemeType,
|
||||
colorScale,
|
||||
});
|
||||
|
||||
const aggFunc = (d: JsonObject) => d.weight || 0;
|
||||
|
||||
const defaultScreenGridColorRange = [
|
||||
[255, 255, 178],
|
||||
[254, 217, 118],
|
||||
[254, 178, 76],
|
||||
[253, 141, 60],
|
||||
[240, 59, 32],
|
||||
[189, 0, 38],
|
||||
] as Color[];
|
||||
|
||||
// Passing a layer creator function instead of a layer since the
|
||||
// layer needs to be regenerated at each render
|
||||
return new ScreenGridLayer({
|
||||
id: `screengrid-layer-${fd.slice_id}` as const,
|
||||
data,
|
||||
cellSizePixels: fd.grid_size,
|
||||
minColor: [c.r, c.g, c.b, 0],
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
colorDomain:
|
||||
colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints && colorRange
|
||||
? [0, colorRange.length]
|
||||
: undefined,
|
||||
colorRange:
|
||||
colorSchemeType === 'default' ? defaultScreenGridColorRange : colorRange,
|
||||
outline: false,
|
||||
getWeight: (d: any) => d.weight || 0,
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setDataMask,
|
||||
@@ -88,6 +111,8 @@ export const getLayer: GetLayerType<ScreenGridLayer> = function ({
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
getWeight: aggFunc,
|
||||
colorScaleType: colorSchemeType === 'default' ? 'linear' : 'quantize',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,11 @@ import {
|
||||
viewport,
|
||||
spatial,
|
||||
mapboxStyle,
|
||||
deckGLFixedColor,
|
||||
deckGLCategoricalColorSchemeSelect,
|
||||
deckGLCategoricalColorSchemeTypeSelect,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
@@ -55,7 +59,28 @@ const config: ControlPanelConfig = {
|
||||
{
|
||||
label: t('Grid'),
|
||||
expanded: true,
|
||||
controlSetRows: [[gridSize, 'color_picker']],
|
||||
controlSetRows: [
|
||||
[gridSize],
|
||||
[
|
||||
{
|
||||
name: 'color_scheme_type',
|
||||
config: {
|
||||
...deckGLCategoricalColorSchemeTypeSelect.config,
|
||||
choices: [
|
||||
['default', 'Default'],
|
||||
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
|
||||
[
|
||||
COLOR_SCHEME_TYPES.categorical_palette,
|
||||
t('Categorical palette'),
|
||||
],
|
||||
],
|
||||
default: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
[deckGLFixedColor],
|
||||
[deckGLCategoricalColorSchemeSelect],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
|
||||
@@ -19,7 +19,15 @@
|
||||
|
||||
import { PickingInfo } from '@deck.gl/core';
|
||||
import { JsonObject, QueryFormData } from '@superset-ui/core';
|
||||
import { getAggFunc, commonLayerProps } from './common';
|
||||
import {
|
||||
getAggFunc,
|
||||
commonLayerProps,
|
||||
getColorForBreakpoints,
|
||||
getColorRange,
|
||||
} from './common';
|
||||
import { ColorBreakpointType } from '../types';
|
||||
import { COLOR_SCHEME_TYPES, ColorSchemeType } from '../utilities/utils';
|
||||
import { DEFAULT_DECKGL_COLOR } from '../utilities/Shared_DeckGL';
|
||||
|
||||
const partialformData: Partial<QueryFormData> = {
|
||||
viz_type: 'table',
|
||||
@@ -159,3 +167,106 @@ describe('commonLayerProps', () => {
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('John Doe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getColorForBreakpoints', () => {
|
||||
const colorBreakpoints: ColorBreakpointType[] = [
|
||||
{ minValue: 0, maxValue: 10, color: { r: 255, g: 0, b: 0, a: 100 } },
|
||||
{ minValue: 11, maxValue: 20, color: { r: 0, g: 255, b: 0, a: 100 } },
|
||||
{ minValue: 21, maxValue: 30, color: { r: 0, g: 0, b: 255, a: 100 } },
|
||||
];
|
||||
|
||||
it('returns correct breakpoint index for value in range', () => {
|
||||
const aggFunc = (arr: number[]) => arr[0];
|
||||
expect(getColorForBreakpoints(aggFunc, [5], colorBreakpoints)).toBe(1);
|
||||
expect(getColorForBreakpoints(aggFunc, [15], colorBreakpoints)).toBe(2);
|
||||
expect(getColorForBreakpoints(aggFunc, [25], colorBreakpoints)).toBe(3);
|
||||
});
|
||||
|
||||
it('returns 0 if value is not in any breakpoint', () => {
|
||||
const aggFunc = () => 100;
|
||||
expect(getColorForBreakpoints(aggFunc, [100], colorBreakpoints)).toBe(0);
|
||||
});
|
||||
|
||||
it('returns undefined if aggFunc returns undefined', () => {
|
||||
const aggFunc = () => undefined;
|
||||
expect(
|
||||
getColorForBreakpoints(aggFunc, [5], colorBreakpoints),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined if aggFunc returns array', () => {
|
||||
const aggFunc = () => [1, 2];
|
||||
expect(
|
||||
getColorForBreakpoints(aggFunc, [5], colorBreakpoints),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getColorRange', () => {
|
||||
const fdBase: any = {
|
||||
color_picker: { r: 10, g: 20, b: 30, a: 0.5 },
|
||||
color_breakpoints: [
|
||||
{ minValue: 0, maxValue: 10, color: { r: 255, g: 0, b: 0, a: 1 } },
|
||||
{ minValue: 11, maxValue: 20, color: { r: 0, g: 255, b: 0, a: 1 } },
|
||||
],
|
||||
};
|
||||
|
||||
it('returns color range for linear_palette', () => {
|
||||
const colorScale = { range: () => ['#ff0000', '#00ff00'] } as any;
|
||||
const result = getColorRange({
|
||||
defaultBreakpointsColor: DEFAULT_DECKGL_COLOR,
|
||||
colorSchemeType: COLOR_SCHEME_TYPES.linear_palette,
|
||||
colorScale,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
[255, 0, 0, 255],
|
||||
[0, 255, 0, 255],
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns color range for categorical_palette', () => {
|
||||
const colorScale = { range: () => ['#0000ff', '#00ffff'] } as any;
|
||||
const result = getColorRange({
|
||||
defaultBreakpointsColor: DEFAULT_DECKGL_COLOR,
|
||||
colorSchemeType: COLOR_SCHEME_TYPES.categorical_palette,
|
||||
colorScale,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
[0, 0, 255, 255],
|
||||
[0, 255, 255, 255],
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns color range for color_breakpoints', () => {
|
||||
const result = getColorRange({
|
||||
colorBreakpoints: fdBase.color_breakpoints,
|
||||
defaultBreakpointsColor: DEFAULT_DECKGL_COLOR,
|
||||
colorSchemeType: COLOR_SCHEME_TYPES.color_breakpoints,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
[
|
||||
DEFAULT_DECKGL_COLOR.r,
|
||||
DEFAULT_DECKGL_COLOR.g,
|
||||
DEFAULT_DECKGL_COLOR.b,
|
||||
DEFAULT_DECKGL_COLOR.a * 255,
|
||||
],
|
||||
[255, 0, 0, 255],
|
||||
[0, 255, 0, 255],
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns default color if color_picker is missing', () => {
|
||||
const result = getColorRange({
|
||||
defaultBreakpointsColor: DEFAULT_DECKGL_COLOR,
|
||||
colorSchemeType: 'unknown_type' as ColorSchemeType,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
[
|
||||
DEFAULT_DECKGL_COLOR.r,
|
||||
DEFAULT_DECKGL_COLOR.g,
|
||||
DEFAULT_DECKGL_COLOR.b,
|
||||
DEFAULT_DECKGL_COLOR.a * 255,
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
deviation as d3deviation,
|
||||
} from 'd3-array';
|
||||
import {
|
||||
CategoricalColorScale,
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
@@ -36,10 +37,15 @@ import {
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
} from '@superset-ui/core';
|
||||
import { Layer, PickingInfo } from '@deck.gl/core';
|
||||
import { Layer, PickingInfo, Color } from '@deck.gl/core';
|
||||
import { ScaleLinear } from 'd3-scale';
|
||||
import { ColorBreakpointType } from '../types';
|
||||
import sandboxedEval from '../utils/sandbox';
|
||||
import { TooltipProps } from '../components/Tooltip';
|
||||
import { getCrossFilterDataMask } from '../utils/crossFiltersDataMask';
|
||||
import { COLOR_SCHEME_TYPES, ColorSchemeType } from '../utilities/utils';
|
||||
import { hexToRGB } from '../utils/colors';
|
||||
import { DEFAULT_DECKGL_COLOR } from '../utilities/Shared_DeckGL';
|
||||
|
||||
export function commonLayerProps({
|
||||
formData,
|
||||
@@ -181,3 +187,87 @@ export function getAggFunc(
|
||||
|
||||
return (arr: number[]) => d3func(arr.map(x => accessor(x)));
|
||||
}
|
||||
|
||||
export const getColorForBreakpoints = (
|
||||
aggFunc: (arr: number[]) => number | number[] | undefined,
|
||||
point: number[],
|
||||
colorBreakpoints: ColorBreakpointType[],
|
||||
) => {
|
||||
const aggResult = aggFunc(point);
|
||||
|
||||
if (aggResult === undefined) return undefined;
|
||||
|
||||
if (Array.isArray(aggResult)) return undefined;
|
||||
|
||||
const breapointForPoint = colorBreakpoints.findIndex(
|
||||
breakpoint =>
|
||||
aggResult >= breakpoint.minValue && aggResult <= breakpoint.maxValue,
|
||||
);
|
||||
|
||||
return breapointForPoint + 1;
|
||||
};
|
||||
|
||||
export const getColorRange = ({
|
||||
colorSchemeType,
|
||||
fixedColor,
|
||||
colorBreakpoints,
|
||||
colorScale,
|
||||
defaultBreakpointsColor,
|
||||
}: {
|
||||
colorSchemeType: ColorSchemeType;
|
||||
defaultBreakpointsColor: { r: number; g: number; b: number; a: number };
|
||||
fixedColor?: { r: number; g: number; b: number; a: number };
|
||||
colorBreakpoints?: ColorBreakpointType[];
|
||||
colorScale?: CategoricalColorScale | ScaleLinear<string, string>;
|
||||
}) => {
|
||||
let colorRange: Color[] | undefined;
|
||||
switch (colorSchemeType) {
|
||||
case COLOR_SCHEME_TYPES.linear_palette:
|
||||
case COLOR_SCHEME_TYPES.categorical_palette: {
|
||||
colorRange = colorScale?.range().map(color => hexToRGB(color)) as Color[];
|
||||
break;
|
||||
}
|
||||
case COLOR_SCHEME_TYPES.color_breakpoints: {
|
||||
const defaultColorArray: Color = defaultBreakpointsColor
|
||||
? [
|
||||
defaultBreakpointsColor.r,
|
||||
defaultBreakpointsColor.g,
|
||||
defaultBreakpointsColor.b,
|
||||
defaultBreakpointsColor.a * 255,
|
||||
]
|
||||
: [
|
||||
DEFAULT_DECKGL_COLOR.r,
|
||||
DEFAULT_DECKGL_COLOR.g,
|
||||
DEFAULT_DECKGL_COLOR.b,
|
||||
DEFAULT_DECKGL_COLOR.a * 255,
|
||||
];
|
||||
|
||||
colorRange = colorBreakpoints?.map(
|
||||
(colorBreakpoint: ColorBreakpointType) =>
|
||||
colorBreakpoint.color
|
||||
? [
|
||||
colorBreakpoint.color.r,
|
||||
colorBreakpoint.color.g,
|
||||
colorBreakpoint.color.b,
|
||||
colorBreakpoint.color.a * 255,
|
||||
]
|
||||
: defaultColorArray,
|
||||
);
|
||||
colorRange?.unshift(defaultColorArray);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const color = fixedColor || {
|
||||
r: DEFAULT_DECKGL_COLOR.r,
|
||||
g: DEFAULT_DECKGL_COLOR.g,
|
||||
b: DEFAULT_DECKGL_COLOR.b,
|
||||
a: DEFAULT_DECKGL_COLOR.a,
|
||||
};
|
||||
|
||||
colorRange = [[color.r, color.g, color.b, color.a * 255]];
|
||||
}
|
||||
}
|
||||
|
||||
return colorRange;
|
||||
};
|
||||
|
||||
@@ -27,3 +27,9 @@ export interface ColorType {
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
export interface ColorBreakpointType {
|
||||
color: ColorType;
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
}
|
||||
|
||||
@@ -25,33 +25,28 @@ import {
|
||||
t,
|
||||
validateNonEmpty,
|
||||
validateMapboxStylesUrl,
|
||||
getCategoricalSchemeRegistry,
|
||||
getSequentialSchemeRegistry,
|
||||
SequentialScheme,
|
||||
} from '@superset-ui/core';
|
||||
import { D3_FORMAT_OPTIONS, sharedControls } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
ControlPanelState,
|
||||
CustomControlItem,
|
||||
D3_FORMAT_OPTIONS,
|
||||
getColorControlsProps,
|
||||
sharedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { columnChoices, PRIMARY_COLOR } from './controls';
|
||||
import {
|
||||
COLOR_SCHEME_TYPES,
|
||||
ColorSchemeType,
|
||||
isColorSchemeTypeVisible,
|
||||
} from './utils';
|
||||
|
||||
let deckglTiles;
|
||||
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
|
||||
const sequentialSchemeRegistry = getSequentialSchemeRegistry();
|
||||
|
||||
export const DEFAULT_DECKGL_TILES = [
|
||||
['https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Streets (OSM)'],
|
||||
['https://tile.osm.ch/osm-swiss-style/{z}/{x}/{y}.png', 'Topography (OSM)'],
|
||||
['mapbox://styles/mapbox/streets-v9', 'Streets (Mapbox)'],
|
||||
['mapbox://styles/mapbox/dark-v9', 'Dark (Mapbox)'],
|
||||
['mapbox://styles/mapbox/light-v9', 'Light (Mapbox)'],
|
||||
['mapbox://styles/mapbox/satellite-streets-v9', 'Satellite Streets (Mapbox)'],
|
||||
['mapbox://styles/mapbox/satellite-v9', 'Satellite (Mapbox)'],
|
||||
['mapbox://styles/mapbox/outdoors-v9', 'Outdoors (Mapbox)'],
|
||||
];
|
||||
|
||||
const getDeckGLTiles = () => {
|
||||
if (!deckglTiles) {
|
||||
const appContainer = document.getElementById('app');
|
||||
const { common } = JSON.parse(
|
||||
appContainer?.getAttribute('data-bootstrap') || '{}',
|
||||
);
|
||||
deckglTiles = common?.deckgl_tiles ?? DEFAULT_DECKGL_TILES;
|
||||
}
|
||||
return deckglTiles;
|
||||
};
|
||||
export const DEFAULT_DECKGL_COLOR = { r: 158, g: 158, b: 158, a: 1 };
|
||||
|
||||
const DEFAULT_VIEWPORT = {
|
||||
longitude: 6.85236157047845,
|
||||
@@ -75,8 +70,8 @@ const jsFunctionInfo = (
|
||||
);
|
||||
|
||||
function jsFunctionControl(
|
||||
label,
|
||||
description,
|
||||
label: string,
|
||||
description: string,
|
||||
extraDescr = null,
|
||||
height = 100,
|
||||
defaultText = '',
|
||||
@@ -127,7 +122,7 @@ export const autozoom = {
|
||||
},
|
||||
};
|
||||
|
||||
export const dimension = {
|
||||
export const dimension: CustomControlItem = {
|
||||
name: 'dimension',
|
||||
config: {
|
||||
...sharedControls.groupby,
|
||||
@@ -220,7 +215,7 @@ export const lineColumn = {
|
||||
label: t('Lines column'),
|
||||
default: null,
|
||||
description: t('The database columns that contains lines information'),
|
||||
mapStateToProps: state => ({
|
||||
mapStateToProps: (state: ControlPanelState) => ({
|
||||
choices: columnChoices(state.datasource),
|
||||
}),
|
||||
validators: [validateNonEmpty],
|
||||
@@ -239,7 +234,7 @@ export const lineWidth = {
|
||||
},
|
||||
};
|
||||
|
||||
export const fillColorPicker = {
|
||||
export const fillColorPicker: CustomControlItem = {
|
||||
name: 'fill_color_picker',
|
||||
config: {
|
||||
label: t('Fill Color'),
|
||||
@@ -249,10 +244,12 @@ export const fillColorPicker = {
|
||||
type: 'ColorPickerControl',
|
||||
default: PRIMARY_COLOR,
|
||||
renderTrigger: true,
|
||||
visibility: ({ controls }) =>
|
||||
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.fixed_color),
|
||||
},
|
||||
};
|
||||
|
||||
export const strokeColorPicker = {
|
||||
export const strokeColorPicker: CustomControlItem = {
|
||||
name: 'stroke_color_picker',
|
||||
config: {
|
||||
label: t('Stroke Color'),
|
||||
@@ -262,6 +259,8 @@ export const strokeColorPicker = {
|
||||
type: 'ColorPickerControl',
|
||||
default: PRIMARY_COLOR,
|
||||
renderTrigger: true,
|
||||
visibility: ({ controls }) =>
|
||||
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.fixed_color),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -331,7 +330,7 @@ export const spatial = {
|
||||
label: t('Longitude & Latitude'),
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Point to your spatial columns'),
|
||||
mapStateToProps: state => ({
|
||||
mapStateToProps: (state: ControlPanelState) => ({
|
||||
choices: columnChoices(state.datasource),
|
||||
}),
|
||||
},
|
||||
@@ -344,7 +343,7 @@ export const pointRadiusFixed = {
|
||||
label: t('Point Size'),
|
||||
default: { type: 'fix', value: 1000 },
|
||||
description: t('Fixed point radius'),
|
||||
mapStateToProps: state => ({
|
||||
mapStateToProps: (state: ControlPanelState) => ({
|
||||
datasource: state.datasource,
|
||||
}),
|
||||
},
|
||||
@@ -396,10 +395,17 @@ export const mapboxStyle = {
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
validators: [validateMapboxStylesUrl],
|
||||
choices: getDeckGLTiles(),
|
||||
default: getDeckGLTiles()[0][0],
|
||||
choices: [
|
||||
['mapbox://styles/mapbox/streets-v9', t('Streets')],
|
||||
['mapbox://styles/mapbox/dark-v9', t('Dark')],
|
||||
['mapbox://styles/mapbox/light-v9', t('Light')],
|
||||
['mapbox://styles/mapbox/satellite-streets-v9', t('Satellite Streets')],
|
||||
['mapbox://styles/mapbox/satellite-v9', t('Satellite')],
|
||||
['mapbox://styles/mapbox/outdoors-v9', t('Outdoors')],
|
||||
],
|
||||
default: 'mapbox://styles/mapbox/light-v9',
|
||||
description: t(
|
||||
'Mapbox base layer map style (see Mapbox documentation: %s) or tile server URL.',
|
||||
'Base layer map style. See Mapbox documentation: %s',
|
||||
'https://docs.mapbox.com/help/glossary/style-url/',
|
||||
),
|
||||
},
|
||||
@@ -412,8 +418,163 @@ export const geojsonColumn = {
|
||||
label: t('GeoJson Column'),
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Select the geojson column'),
|
||||
mapStateToProps: state => ({
|
||||
mapStateToProps: (state: ControlPanelState) => ({
|
||||
choices: columnChoices(state.datasource),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const deckGLCategoricalColorSchemeTypeSelect: CustomControlItem = {
|
||||
name: 'color_scheme_type',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Color Scheme Type'),
|
||||
description: t('Select the type of color scheme to use.'),
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
validators: [],
|
||||
choices: [
|
||||
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
|
||||
[COLOR_SCHEME_TYPES.categorical_palette, t('Categorical palette')],
|
||||
[COLOR_SCHEME_TYPES.color_breakpoints, t('Color breakpoints')],
|
||||
],
|
||||
default: COLOR_SCHEME_TYPES.categorical_palette,
|
||||
},
|
||||
};
|
||||
|
||||
export const deckGLFixedColor: CustomControlItem = {
|
||||
name: 'color_picker',
|
||||
config: {
|
||||
type: 'ColorPickerControl',
|
||||
label: t('Fixed Color'),
|
||||
default: PRIMARY_COLOR,
|
||||
renderTrigger: true,
|
||||
description: t('Select the fixed color'),
|
||||
visibility: ({ controls }) =>
|
||||
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.fixed_color),
|
||||
},
|
||||
};
|
||||
|
||||
export const deckGLCategoricalColor: CustomControlItem = {
|
||||
name: dimension.name,
|
||||
config: {
|
||||
...dimension.config,
|
||||
label: t('Categorical Color'),
|
||||
description: t(
|
||||
'Pick a dimension from which categorical colors are defined',
|
||||
),
|
||||
visibility: ({ controls }) =>
|
||||
isColorSchemeTypeVisible(
|
||||
controls,
|
||||
COLOR_SCHEME_TYPES.categorical_palette,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const deckGLCategoricalColorSchemeSelect: CustomControlItem = {
|
||||
name: 'color_scheme',
|
||||
config: {
|
||||
type: 'ColorSchemeControl',
|
||||
label: t('Color Scheme'),
|
||||
default: categoricalSchemeRegistry.getDefaultKey(),
|
||||
renderTrigger: true,
|
||||
choices: () => categoricalSchemeRegistry.keys().map(s => [s, s]),
|
||||
description: t('The color scheme for rendering chart'),
|
||||
schemes: () => categoricalSchemeRegistry.getMap(),
|
||||
visibility: ({ controls }) =>
|
||||
isColorSchemeTypeVisible(
|
||||
controls,
|
||||
COLOR_SCHEME_TYPES.categorical_palette,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const deckGLLinearColorSchemeSelect: CustomControlItem = {
|
||||
name: 'linear_color_scheme',
|
||||
config: {
|
||||
type: 'ColorSchemeControl',
|
||||
label: t('Linear Color Scheme'),
|
||||
choices: () =>
|
||||
(sequentialSchemeRegistry.values() as SequentialScheme[]).map(value => [
|
||||
value.id,
|
||||
value.label,
|
||||
]),
|
||||
default: sequentialSchemeRegistry.getDefaultKey(),
|
||||
clearable: false,
|
||||
description: t('Select a linear color scheme'),
|
||||
renderTrigger: true,
|
||||
schemes: () => sequentialSchemeRegistry.getMap(),
|
||||
isLinear: true,
|
||||
mapStateToProps: state => getColorControlsProps(state),
|
||||
visibility: ({ controls }) =>
|
||||
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.linear_palette),
|
||||
},
|
||||
};
|
||||
|
||||
export const deckGLColorBreakpointsSelect: CustomControlItem = {
|
||||
name: 'color_breakpoints',
|
||||
config: {
|
||||
label: t('Color breakpoints'),
|
||||
type: 'ColorBreakpointsControl',
|
||||
description: t('Define color breakpoints for the data'),
|
||||
renderTrigger: true,
|
||||
visibility: ({ controls }) =>
|
||||
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.color_breakpoints),
|
||||
},
|
||||
};
|
||||
|
||||
export const breakpointsDefaultColor: CustomControlItem = {
|
||||
name: 'deafult_breakpoint_color',
|
||||
config: {
|
||||
label: t('Default color'),
|
||||
type: 'ColorPickerControl',
|
||||
description: t(
|
||||
"The color used when a value doesn't match any defined breakpoints.",
|
||||
),
|
||||
default: DEFAULT_DECKGL_COLOR,
|
||||
renderTrigger: true,
|
||||
visibility: ({ controls }) =>
|
||||
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.color_breakpoints),
|
||||
},
|
||||
};
|
||||
|
||||
export const deckGLCategoricalColorSchemeControls = [
|
||||
[deckGLCategoricalColorSchemeTypeSelect],
|
||||
[deckGLFixedColor],
|
||||
[deckGLCategoricalColor],
|
||||
[deckGLCategoricalColorSchemeSelect],
|
||||
[deckGLColorBreakpointsSelect],
|
||||
];
|
||||
|
||||
export const generateDeckGLColorSchemeControls = ({
|
||||
defaultSchemeType,
|
||||
disableCategoricalColumn = false,
|
||||
}: {
|
||||
defaultSchemeType?: ColorSchemeType;
|
||||
disableCategoricalColumn?: boolean;
|
||||
}) => [
|
||||
[
|
||||
{
|
||||
name: 'color_scheme_type',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Color Scheme Type'),
|
||||
description: t('Select the type of color scheme to use.'),
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
validators: [],
|
||||
choices: [
|
||||
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
|
||||
[COLOR_SCHEME_TYPES.categorical_palette, t('Categorical palette')],
|
||||
[COLOR_SCHEME_TYPES.color_breakpoints, t('Color breakpoints')],
|
||||
],
|
||||
default: defaultSchemeType || COLOR_SCHEME_TYPES.categorical_palette,
|
||||
},
|
||||
},
|
||||
],
|
||||
[deckGLFixedColor],
|
||||
disableCategoricalColumn ? [] : [deckGLCategoricalColor],
|
||||
[deckGLCategoricalColorSchemeSelect],
|
||||
[breakpointsDefaultColor],
|
||||
[deckGLColorBreakpointsSelect],
|
||||
];
|
||||
@@ -16,8 +16,25 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ControlStateMapping } from '@superset-ui/chart-controls';
|
||||
|
||||
export const COLOR_SCHEME_TYPES = {
|
||||
fixed_color: 'fixed_color',
|
||||
categorical_palette: 'categorical_palette',
|
||||
linear_palette: 'linear_palette',
|
||||
color_breakpoints: 'color_breakpoints',
|
||||
} as const;
|
||||
|
||||
export type ColorSchemeType =
|
||||
(typeof COLOR_SCHEME_TYPES)[keyof typeof COLOR_SCHEME_TYPES];
|
||||
|
||||
/* eslint camelcase: 0 */
|
||||
|
||||
export function formatSelectOptions(options: (string | number)[]) {
|
||||
return options.map(opt => [opt, opt.toString()]);
|
||||
}
|
||||
|
||||
export const isColorSchemeTypeVisible = (
|
||||
controls: ControlStateMapping,
|
||||
colorSchemeType: ColorSchemeType,
|
||||
) => controls.color_scheme_type.value === colorSchemeType;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 { getColorBreakpointsBuckets } from './utils';
|
||||
import { ColorBreakpointType } from './types';
|
||||
|
||||
describe('getColorBreakpointsBuckets', () => {
|
||||
it('returns correct buckets for multiple breakpoints', () => {
|
||||
const color_breakpoints: ColorBreakpointType[] = [
|
||||
{ minValue: 0, maxValue: 10, color: { r: 255, g: 0, b: 0, a: 100 } },
|
||||
{ minValue: 11, maxValue: 20, color: { r: 0, g: 255, b: 0, a: 100 } },
|
||||
{ minValue: 21, maxValue: 30, color: { r: 0, g: 0, b: 255, a: 100 } },
|
||||
];
|
||||
const result = getColorBreakpointsBuckets(color_breakpoints);
|
||||
expect(result).toEqual({
|
||||
'0 - 10': { color: [255, 0, 0], enabled: true },
|
||||
'11 - 20': { color: [0, 255, 0], enabled: true },
|
||||
'21 - 30': { color: [0, 0, 255], enabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty object if color_breakpoints is empty', () => {
|
||||
const result = getColorBreakpointsBuckets([]);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('returns empty object if color_breakpoints is missing', () => {
|
||||
const result = getColorBreakpointsBuckets({} as any);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -25,16 +25,12 @@ import {
|
||||
QueryFormData,
|
||||
SequentialScheme,
|
||||
} from '@superset-ui/core';
|
||||
import { GeoBoundingBox, TileLayer } from '@deck.gl/geo-layers';
|
||||
import { BitmapLayer, PathLayer } from '@deck.gl/layers';
|
||||
import { Color } from '@deck.gl/core';
|
||||
import { hexToRGB } from './utils/colors';
|
||||
import { ColorBreakpointType } from './types';
|
||||
|
||||
const DEFAULT_NUM_BUCKETS = 10;
|
||||
|
||||
export const MAPBOX_LAYER_PREFIX = 'mapbox://';
|
||||
export const TILE_LAYER_PREFIX = 'tile://';
|
||||
export const OSM_LAYER_KEYWORDS = ['openstreetmap', 'osm'];
|
||||
|
||||
export type Buckets = {
|
||||
break_points: string[];
|
||||
num_buckets: string;
|
||||
@@ -99,7 +95,7 @@ export function getBreakPointColorScaler(
|
||||
}: BucketsWithColorScale,
|
||||
features: JsonObject[],
|
||||
accessor: (value: JsonObject) => number | undefined,
|
||||
) {
|
||||
): (data?: JsonObject) => Color {
|
||||
const breakPoints =
|
||||
formDataBreakPoints || formDataNumBuckets
|
||||
? getBreakPoints(
|
||||
@@ -119,7 +115,7 @@ export function getBreakPointColorScaler(
|
||||
: getSequentialSchemeRegistry().get(linearColorScheme);
|
||||
|
||||
if (!colorScheme) {
|
||||
return null;
|
||||
return () => [0, 0, 0, 0];
|
||||
}
|
||||
let scaler: ScaleLinear<string, string> | ScaleThreshold<number, string>;
|
||||
let maskPoint: (v: number | undefined) => boolean;
|
||||
@@ -155,7 +151,7 @@ export function getBreakPointColorScaler(
|
||||
maskPoint = () => false;
|
||||
}
|
||||
|
||||
return (d: JsonObject): [number, number, number, number] => {
|
||||
return (d: JsonObject): Color => {
|
||||
const v = accessor(d);
|
||||
if (!v) {
|
||||
return [0, 0, 0, 0];
|
||||
@@ -180,7 +176,7 @@ export function getBuckets(
|
||||
const colorScaler = getBreakPointColorScaler(fd, features, accessor);
|
||||
const buckets: Record<
|
||||
string,
|
||||
{ color: [number, number, number, number] | undefined; enabled: boolean }
|
||||
{ color: Color | undefined; enabled: boolean }
|
||||
> = {};
|
||||
breakPoints.slice(1).forEach((_, i) => {
|
||||
const range = `${breakPoints[i]} - ${breakPoints[i + 1]}`;
|
||||
@@ -197,41 +193,25 @@ export function getBuckets(
|
||||
return buckets;
|
||||
}
|
||||
|
||||
export function buildTileLayer(url: string, id: string) {
|
||||
interface TileLayerProps {
|
||||
id: string;
|
||||
data: string;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
tileSize: number;
|
||||
renderSubLayers: (props: any) => (BitmapLayer | PathLayer)[];
|
||||
export function getColorBreakpointsBuckets(
|
||||
colorBreakpoints: ColorBreakpointType[],
|
||||
) {
|
||||
const breakpoints = colorBreakpoints || [];
|
||||
|
||||
const buckets: Record<string, { color: Color; enabled: boolean }> = {};
|
||||
|
||||
if (!breakpoints || !breakpoints.length) {
|
||||
return buckets;
|
||||
}
|
||||
|
||||
interface RenderSubLayerProps {
|
||||
tile: {
|
||||
bbox: GeoBoundingBox;
|
||||
breakpoints.forEach((breakpoint: ColorBreakpointType) => {
|
||||
const range = `${breakpoint.minValue} - ${breakpoint.maxValue}`;
|
||||
|
||||
buckets[range] = {
|
||||
color: [breakpoint.color.r, breakpoint.color.g, breakpoint.color.b],
|
||||
enabled: true,
|
||||
};
|
||||
data: any;
|
||||
}
|
||||
});
|
||||
|
||||
return new TileLayer({
|
||||
data: url,
|
||||
id,
|
||||
minZoom: 0,
|
||||
maxZoom: 19,
|
||||
tileSize: 256,
|
||||
|
||||
renderSubLayers: (props: RenderSubLayerProps): BitmapLayer[] => {
|
||||
const { west, north, east, south } = props.tile.bbox as GeoBoundingBox;
|
||||
|
||||
// Ajouter une BitmapLayer
|
||||
const bitmapLayer = new BitmapLayer(props, {
|
||||
data: undefined,
|
||||
image: props.data,
|
||||
bounds: [west, south, east, north],
|
||||
});
|
||||
|
||||
return [bitmapLayer];
|
||||
},
|
||||
} as TileLayerProps);
|
||||
return buckets;
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ const processColumns = memoizeOne(function processColumns(
|
||||
// percent metrics have a default format
|
||||
formatter = getNumberFormatter(numberFormat || PERCENT_3_POINT);
|
||||
} else if (isMetric || (isNumber && (numberFormat || currency))) {
|
||||
formatter = currency
|
||||
formatter = currency?.symbol
|
||||
? new CurrencyFormatter({
|
||||
d3Format: numberFormat,
|
||||
currency,
|
||||
|
||||
@@ -359,6 +359,22 @@ const config: ControlPanelConfig = {
|
||||
[xAxisLabelRotation],
|
||||
[xAxisLabelInterval],
|
||||
...richTooltipSection,
|
||||
[
|
||||
{
|
||||
name: 'show_query_identifiers',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show query identifiers'),
|
||||
description: t(
|
||||
'Adds Query A and Query B identifiers to help differentiate series',
|
||||
),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
visibility: ({ controls }) =>
|
||||
Boolean(controls?.rich_tooltip?.value),
|
||||
},
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
[<ControlSubSectionHeader>{t('Y Axis')}</ControlSubSectionHeader>],
|
||||
[
|
||||
|
||||
@@ -212,6 +212,7 @@ export default function transformProps(
|
||||
sortSeriesAscendingB,
|
||||
timeGrainSqla,
|
||||
percentageThreshold,
|
||||
show_query_identifiers = false,
|
||||
metrics = [],
|
||||
metricsB = [],
|
||||
}: EchartsMixedTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
||||
@@ -395,10 +396,19 @@ export default function transformProps(
|
||||
const seriesName = inverted[entryName] || entryName;
|
||||
const colorScaleKey = getOriginalSeries(seriesName, array);
|
||||
|
||||
let displayName = `${entryName} (Query A)`;
|
||||
let displayName: string;
|
||||
|
||||
if (groupby.length > 0) {
|
||||
displayName = `${MetricDisplayNameA} (Query A), ${entryName}`;
|
||||
// When we have groupby, format as "metric, dimension"
|
||||
const metricPart = show_query_identifiers
|
||||
? `${MetricDisplayNameA} (Query A)`
|
||||
: MetricDisplayNameA;
|
||||
displayName = `${metricPart}, ${entryName}`;
|
||||
} else {
|
||||
// When no groupby, format as just the entry name with optional query identifier
|
||||
displayName = show_query_identifiers
|
||||
? `${entryName} (Query A)`
|
||||
: entryName;
|
||||
}
|
||||
|
||||
const seriesFormatter = getFormatter(
|
||||
@@ -453,10 +463,19 @@ export default function transformProps(
|
||||
const seriesName = `${seriesEntry} (1)`;
|
||||
const colorScaleKey = getOriginalSeries(seriesEntry, array);
|
||||
|
||||
let displayName = `${entryName} (Query B)`;
|
||||
let displayName: string;
|
||||
|
||||
if (groupbyB.length > 0) {
|
||||
displayName = `${MetricDisplayNameB} (Query B), ${entryName}`;
|
||||
// When we have groupby, format as "metric, dimension"
|
||||
const metricPart = show_query_identifiers
|
||||
? `${MetricDisplayNameB} (Query B)`
|
||||
: MetricDisplayNameB;
|
||||
displayName = `${metricPart}, ${entryName}`;
|
||||
} else {
|
||||
// When no groupby, format as just the entry name with optional query identifier
|
||||
displayName = show_query_identifiers
|
||||
? `${entryName} (Query B)`
|
||||
: entryName;
|
||||
}
|
||||
|
||||
const seriesFormatter = getFormatter(
|
||||
|
||||
@@ -60,6 +60,7 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & {
|
||||
tooltipTimeFormat?: string;
|
||||
zoomable: boolean;
|
||||
richTooltip: boolean;
|
||||
show_query_identifiers?: boolean;
|
||||
xAxisLabelRotation: number;
|
||||
xAxisLabelInterval?: number | string;
|
||||
colorScheme?: string;
|
||||
@@ -133,6 +134,7 @@ export const DEFAULT_FORM_DATA: EchartsMixedTimeseriesFormData = {
|
||||
groupbyB: [],
|
||||
zoomable: TIMESERIES_DEFAULTS.zoomable,
|
||||
richTooltip: TIMESERIES_DEFAULTS.richTooltip,
|
||||
show_query_identifiers: false,
|
||||
xAxisLabelRotation: TIMESERIES_DEFAULTS.xAxisLabelRotation,
|
||||
xAxisLabelInterval: TIMESERIES_DEFAULTS.xAxisLabelInterval,
|
||||
...DEFAULT_TITLE_FORM_DATA,
|
||||
|
||||
@@ -209,12 +209,18 @@ export default function EchartsTimeseries({
|
||||
}),
|
||||
);
|
||||
groupBy.forEach((dimension, i) => {
|
||||
const val = labelMap[seriesName][i];
|
||||
const dimensionValues = labelMap[seriesName] ?? [];
|
||||
|
||||
// Skip the metric values at the beginning and get the actual dimension value
|
||||
// If we have multiple metrics, they come first, then the dimension values
|
||||
const metricsCount = dimensionValues.length - groupBy.length;
|
||||
const val = dimensionValues[metricsCount + i];
|
||||
|
||||
drillByFilters.push({
|
||||
col: dimension,
|
||||
op: '==',
|
||||
val,
|
||||
formattedVal: formatSeriesName(values[i], {
|
||||
formattedVal: formatSeriesName(val, {
|
||||
timeFormatter: getTimeFormatter(formData.dateFormat),
|
||||
numberFormatter: getNumberFormatter(formData.numberFormat),
|
||||
coltype: coltypeMapping?.[getColumnLabel(dimension)],
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
EchartsMixedTimeseriesProps,
|
||||
} from '../../src/MixedTimeseries/types';
|
||||
|
||||
const formData: EchartsMixedTimeseriesFormData = {
|
||||
const baseFormData: EchartsMixedTimeseriesFormData = {
|
||||
annotationLayers: [],
|
||||
area: false,
|
||||
areaB: false,
|
||||
@@ -81,6 +81,7 @@ const formData: EchartsMixedTimeseriesFormData = {
|
||||
forecastPeriods: [],
|
||||
forecastInterval: 0,
|
||||
forecastSeasonalityDaily: 0,
|
||||
show_query_identifiers: false,
|
||||
};
|
||||
|
||||
const queriesData = [
|
||||
@@ -108,57 +109,118 @@ const queriesData = [
|
||||
},
|
||||
];
|
||||
|
||||
const chartPropsConfig = {
|
||||
formData,
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData,
|
||||
theme: supersetTheme,
|
||||
};
|
||||
describe('MixedTimeseries transformProps', () => {
|
||||
it('should transform chart props for viz with query identifiers disabled', () => {
|
||||
const formData = { ...baseFormData, show_query_identifiers: false };
|
||||
const chartPropsConfig = {
|
||||
formData,
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData,
|
||||
theme: supersetTheme,
|
||||
};
|
||||
const chartProps = new ChartProps({ ...chartPropsConfig, formData });
|
||||
const transformed = transformProps(
|
||||
chartProps as EchartsMixedTimeseriesProps,
|
||||
);
|
||||
|
||||
it('should transform chart props for viz', () => {
|
||||
const chartProps = new ChartProps(chartPropsConfig);
|
||||
const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps);
|
||||
|
||||
expect(transformed).toEqual(
|
||||
expect.objectContaining({
|
||||
echartOptions: expect.objectContaining({
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 1],
|
||||
[599916000000, 3],
|
||||
],
|
||||
id: 'sum__num (Query A), boy',
|
||||
stack: 'obs\na',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 2],
|
||||
[599916000000, 4],
|
||||
],
|
||||
id: 'sum__num (Query A), girl',
|
||||
stack: 'obs\na',
|
||||
}),
|
||||
// Query B — Bar series
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 1],
|
||||
[599916000000, 3],
|
||||
],
|
||||
id: 'sum__num (Query B), boy',
|
||||
stack: 'obs\nb',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 2],
|
||||
[599916000000, 4],
|
||||
],
|
||||
id: 'sum__num (Query B), girl',
|
||||
stack: 'obs\nb',
|
||||
}),
|
||||
]),
|
||||
expect(transformed).toEqual(
|
||||
expect.objectContaining({
|
||||
echartOptions: expect.objectContaining({
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 1],
|
||||
[599916000000, 3],
|
||||
],
|
||||
id: 'sum__num, boy',
|
||||
stack: 'obs\na',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 2],
|
||||
[599916000000, 4],
|
||||
],
|
||||
id: 'sum__num, girl',
|
||||
stack: 'obs\na',
|
||||
}),
|
||||
// Query B — Bar series
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 1],
|
||||
[599916000000, 3],
|
||||
],
|
||||
id: 'sum__num, boy',
|
||||
stack: 'obs\nb',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 2],
|
||||
[599916000000, 4],
|
||||
],
|
||||
id: 'sum__num, girl',
|
||||
stack: 'obs\nb',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform chart props for viz with query identifiers enabled', () => {
|
||||
const formData = { ...baseFormData, show_query_identifiers: true };
|
||||
const chartPropsConfig = {
|
||||
formData,
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData,
|
||||
theme: supersetTheme,
|
||||
};
|
||||
const chartProps = new ChartProps({ ...chartPropsConfig, formData });
|
||||
const transformed = transformProps(
|
||||
chartProps as EchartsMixedTimeseriesProps,
|
||||
);
|
||||
|
||||
expect(transformed).toEqual(
|
||||
expect.objectContaining({
|
||||
echartOptions: expect.objectContaining({
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 1],
|
||||
[599916000000, 3],
|
||||
],
|
||||
id: 'sum__num (Query A), boy',
|
||||
stack: 'obs\na',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 2],
|
||||
[599916000000, 4],
|
||||
],
|
||||
id: 'sum__num (Query A), girl',
|
||||
stack: 'obs\na',
|
||||
}),
|
||||
// Query B — Bar series
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 1],
|
||||
[599916000000, 3],
|
||||
],
|
||||
id: 'sum__num (Query B), boy',
|
||||
stack: 'obs\nb',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 2],
|
||||
[599916000000, 4],
|
||||
],
|
||||
id: 'sum__num (Query B), girl',
|
||||
stack: 'obs\nb',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -276,7 +276,7 @@ const processColumns = memoizeOne(function processColumns(
|
||||
// percent metrics have a default format
|
||||
formatter = getNumberFormatter(numberFormat || PERCENT_3_POINT);
|
||||
} else if (isMetric || (isNumber && (numberFormat || currency))) {
|
||||
formatter = currency
|
||||
formatter = currency?.symbol
|
||||
? new CurrencyFormatter({
|
||||
d3Format: numberFormat,
|
||||
currency,
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
export NODE_NO_WARNINGS=1
|
||||
|
||||
for file in $( find ../superset/translations/** -name '*.po' );
|
||||
do
|
||||
extension=${file##*.}
|
||||
@@ -29,7 +31,7 @@ do
|
||||
if [ $extension == "po" ]
|
||||
then
|
||||
echo "po2json --domain superset --format jed1.x $file $filename.json"
|
||||
po2json --domain superset --format jed1.x $file $filename.json
|
||||
po2json --domain superset --format jed1.x --fuzzy $file $filename.json
|
||||
prettier --write $filename.json
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -926,7 +926,7 @@ const SqlEditor: FC<Props> = ({
|
||||
css={css`
|
||||
margin-bottom: ${theme.sizeUnit * 2}px;
|
||||
padding-top: ${theme.sizeUnit * 4}px;
|
||||
.antd5-alert-action {
|
||||
.ant-alert-action {
|
||||
align-self: center;
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -353,7 +353,7 @@ test('Sends the correct db when changing the database', async () => {
|
||||
await waitFor(() =>
|
||||
expect(props.onDbChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: `mysql-test-mysql-2`,
|
||||
value: 2,
|
||||
database_name: 'test-mysql',
|
||||
backend: 'mysql',
|
||||
}),
|
||||
|
||||
@@ -319,11 +319,14 @@ export function DatabaseSelector({
|
||||
value: { label: string; value: number },
|
||||
database: DatabaseValue,
|
||||
) {
|
||||
setCurrentDb(database);
|
||||
// the database id is actually stored in the value property; the ID is used
|
||||
// for the DOM, so it can't be an integer
|
||||
const databaseWithId = { ...database, id: database.value };
|
||||
setCurrentDb(databaseWithId);
|
||||
setCurrentCatalog(undefined);
|
||||
setCurrentSchema(undefined);
|
||||
if (onDbChange) {
|
||||
onDbChange(database);
|
||||
onDbChange(databaseWithId);
|
||||
}
|
||||
if (onCatalogChange) {
|
||||
onCatalogChange(undefined);
|
||||
|
||||
@@ -717,7 +717,9 @@ class DatasourceEditor extends PureComponent {
|
||||
}
|
||||
|
||||
onDatasourceTypeChange(datasourceType) {
|
||||
this.setState({ datasourceType });
|
||||
// Call onChange after setting datasourceType to ensure
|
||||
// SQL is cleared when switching to a physical dataset
|
||||
this.setState({ datasourceType }, this.onChange);
|
||||
}
|
||||
|
||||
setColumns(obj) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
screen,
|
||||
waitFor,
|
||||
userEvent,
|
||||
cleanup,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import mockDatasource from 'spec/fixtures/mockDatasource';
|
||||
import { isFeatureEnabled } from '@superset-ui/core';
|
||||
@@ -244,4 +245,36 @@ describe('DatasourceEditor Source Tab', () => {
|
||||
expect(physicalRadioBtn).toBeDisabled();
|
||||
expect(virtualRadioBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('calls onChange with empty SQL when switching to physical dataset', async () => {
|
||||
// Clean previous render
|
||||
cleanup();
|
||||
|
||||
props.onChange.mockClear();
|
||||
|
||||
await asyncRender({
|
||||
...props,
|
||||
datasource: {
|
||||
...props.datasource,
|
||||
table_name: 'Vehicle Sales +',
|
||||
datasourceType: 'virtual',
|
||||
sql: 'SELECT * FROM users',
|
||||
},
|
||||
});
|
||||
|
||||
// Enable edit mode
|
||||
const getLockBtn = screen.getByRole('img', { name: /lock/i });
|
||||
await userEvent.click(getLockBtn);
|
||||
|
||||
// Switch to physical dataset
|
||||
const physicalRadioBtn = screen.getByRole('radio', {
|
||||
name: /physical \(table or view\)/i,
|
||||
});
|
||||
await userEvent.click(physicalRadioBtn);
|
||||
|
||||
// Assert that the latest onChange call has empty SQL
|
||||
expect(props.onChange).toHaveBeenCalled();
|
||||
const updatedDatasource = props.onChange.mock.calls[0];
|
||||
expect(updatedDatasource[0].sql).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,6 +70,7 @@ export const ErrorAlert: React.FC<ErrorAlertProps> = ({
|
||||
const preStyle = {
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: theme.fontFamilyCode,
|
||||
margin: `${theme.sizeUnit}px 0`,
|
||||
};
|
||||
const renderDescription = () => (
|
||||
<div>
|
||||
|
||||
@@ -92,7 +92,7 @@ export function MarshmallowErrorMessage({
|
||||
children: (
|
||||
<JSONTree
|
||||
data={extra.messages}
|
||||
shouldExpandNode={() => true}
|
||||
shouldExpandNodeInitially={() => true}
|
||||
hideRoot
|
||||
theme={jsonTreeTheme}
|
||||
/>
|
||||
|
||||
@@ -47,6 +47,15 @@ export const TagsList = ({
|
||||
onDelete,
|
||||
maxTags,
|
||||
}: TagsListProps) => {
|
||||
const sortedTags = useMemo(
|
||||
() =>
|
||||
tags.toSorted((a, b) => {
|
||||
const nameA = a.name ?? '';
|
||||
const nameB = b.name ?? '';
|
||||
return nameA.localeCompare(nameB);
|
||||
}),
|
||||
[tags],
|
||||
);
|
||||
const [tempMaxTags, setTempMaxTags] = useState<number | undefined>(maxTags);
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
@@ -58,21 +67,23 @@ export const TagsList = ({
|
||||
const collapse = () => setTempMaxTags(maxTags);
|
||||
|
||||
const tagsIsLong: boolean | null = useMemo(
|
||||
() => (tempMaxTags ? tags.length > tempMaxTags : null),
|
||||
[tags.length, tempMaxTags],
|
||||
() => (tempMaxTags ? sortedTags.length > tempMaxTags : null),
|
||||
[sortedTags.length, tempMaxTags],
|
||||
);
|
||||
|
||||
const extraTags: number | null = useMemo(
|
||||
() =>
|
||||
typeof tempMaxTags === 'number' ? tags.length - tempMaxTags + 1 : null,
|
||||
[tagsIsLong, tags.length, tempMaxTags],
|
||||
typeof tempMaxTags === 'number'
|
||||
? sortedTags.length - tempMaxTags + 1
|
||||
: null,
|
||||
[tagsIsLong, sortedTags.length, tempMaxTags],
|
||||
);
|
||||
|
||||
return (
|
||||
<TagsDiv className="tag-list">
|
||||
{tagsIsLong && typeof tempMaxTags === 'number' ? (
|
||||
<>
|
||||
{tags.slice(0, tempMaxTags - 1).map((tag: TagType, index) => (
|
||||
{sortedTags.slice(0, tempMaxTags - 1).map((tag: TagType, index) => (
|
||||
<Tag
|
||||
id={tag.id}
|
||||
key={tag.id}
|
||||
@@ -82,17 +93,17 @@ export const TagsList = ({
|
||||
editable={editable}
|
||||
/>
|
||||
))}
|
||||
{tags.length > tempMaxTags ? (
|
||||
{sortedTags.length > tempMaxTags ? (
|
||||
<Tag
|
||||
name={`+${extraTags}...`}
|
||||
onClick={expand}
|
||||
toolTipTitle={tags.map(t => t.name).join(', ')}
|
||||
toolTipTitle={sortedTags.map(t => t.name).join(', ')}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{tags.map((tag: TagType, index) => (
|
||||
{sortedTags.map((tag: TagType, index) => (
|
||||
<Tag
|
||||
id={tag.id}
|
||||
key={tag.id}
|
||||
@@ -103,7 +114,7 @@ export const TagsList = ({
|
||||
/>
|
||||
))}
|
||||
{maxTags ? (
|
||||
tags.length > maxTags ? (
|
||||
sortedTags.length > maxTags ? (
|
||||
<Tag name="..." onClick={collapse} />
|
||||
) : null
|
||||
) : null}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { DEFAULT_D3_FORMAT, DEFAULT_D3_TIME_FORMAT } from '@superset-ui/core';
|
||||
|
||||
import { BootstrapData, CommonBootstrapData } from './types/bootstrapTypes';
|
||||
|
||||
export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ';
|
||||
@@ -141,7 +140,11 @@ export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
|
||||
},
|
||||
extra_categorical_color_schemes: [],
|
||||
extra_sequential_color_schemes: [],
|
||||
theme: {},
|
||||
theme: {
|
||||
default: {},
|
||||
dark: {},
|
||||
settings: {},
|
||||
},
|
||||
menu_data: {
|
||||
menu: [],
|
||||
brand: {
|
||||
|
||||
@@ -294,6 +294,7 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
owners,
|
||||
roles,
|
||||
slug,
|
||||
tags,
|
||||
} = data;
|
||||
|
||||
const hasId = item => item.id !== undefined;
|
||||
@@ -315,6 +316,9 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
? undefined
|
||||
: ensureIsArray(roles).map(r => (hasId(r) ? r.id : r)),
|
||||
slug: slug || null,
|
||||
tags: !isFeatureEnabled(FeatureFlag.TaggingSystem)
|
||||
? undefined
|
||||
: ensureIsArray(tags || []).map(r => (hasId(r) ? r.id : r)),
|
||||
metadata: {
|
||||
...data.metadata,
|
||||
color_namespace: getColorNamespace(data.metadata?.color_namespace),
|
||||
@@ -445,6 +449,7 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
slug: cleanedData.slug,
|
||||
owners: cleanedData.owners,
|
||||
roles: cleanedData.roles,
|
||||
tags: cleanedData.tags || [],
|
||||
json_metadata: safeStringify({
|
||||
...(cleanedData?.metadata || {}),
|
||||
default_filters: safeStringify(serializedFilters),
|
||||
|
||||
@@ -23,6 +23,11 @@ import {
|
||||
within,
|
||||
screen,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { FeatureFlag } from '@superset-ui/core';
|
||||
import {
|
||||
OPEN_FILTER_BAR_WIDTH,
|
||||
CLOSED_FILTER_BAR_WIDTH,
|
||||
} from 'src/dashboard/constants';
|
||||
import DashboardBuilder from 'src/dashboard/components/DashboardBuilder/DashboardBuilder';
|
||||
import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth';
|
||||
import {
|
||||
@@ -268,6 +273,46 @@ describe('DashboardBuilder', () => {
|
||||
expect(filterbar).toHaveStyleRule('width', `${expectedValue}px`);
|
||||
});
|
||||
|
||||
it('filter panel state when featureflag is true', () => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.FilterBarClosedByDefault]: true,
|
||||
};
|
||||
const setter = jest.fn();
|
||||
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
|
||||
CLOSED_FILTER_BAR_WIDTH,
|
||||
setter,
|
||||
]);
|
||||
const { getByTestId } = setup({
|
||||
dashboardInfo: {
|
||||
...mockState.dashboardInfo,
|
||||
dash_edit_perm: true,
|
||||
},
|
||||
});
|
||||
|
||||
const filterbar = getByTestId('dashboard-filters-panel');
|
||||
expect(filterbar).toHaveStyleRule('width', `${CLOSED_FILTER_BAR_WIDTH}px`);
|
||||
});
|
||||
|
||||
it('filter panel state when featureflag is false', () => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.FilterBarClosedByDefault]: false,
|
||||
};
|
||||
const setter = jest.fn();
|
||||
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
|
||||
OPEN_FILTER_BAR_WIDTH,
|
||||
setter,
|
||||
]);
|
||||
const { getByTestId } = setup({
|
||||
dashboardInfo: {
|
||||
...mockState.dashboardInfo,
|
||||
dash_edit_perm: true,
|
||||
},
|
||||
});
|
||||
|
||||
const filterbar = getByTestId('dashboard-filters-panel');
|
||||
expect(filterbar).toHaveStyleRule('width', `${OPEN_FILTER_BAR_WIDTH}px`);
|
||||
});
|
||||
|
||||
it('should not render the filter bar when nativeFiltersEnabled is false', () => {
|
||||
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
|
||||
showDashboard: true,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
|
||||
import {
|
||||
useFilters,
|
||||
useNativeFiltersDataMask,
|
||||
@@ -71,6 +72,8 @@ export const useNativeFilters = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(isFeatureEnabled(FeatureFlag.FilterBarClosedByDefault) &&
|
||||
expandFilters === null) ||
|
||||
expandFilters === false ||
|
||||
(filterValues.length === 0 && nativeFiltersEnabled)
|
||||
) {
|
||||
|
||||
@@ -415,6 +415,7 @@ const Header = () => {
|
||||
owners: dashboardInfo.owners,
|
||||
roles: dashboardInfo.roles,
|
||||
slug,
|
||||
tags: dashboardInfo.tags,
|
||||
metadata: {
|
||||
...dashboardInfo?.metadata,
|
||||
color_namespace: currentColorNamespace,
|
||||
@@ -525,6 +526,7 @@ const Header = () => {
|
||||
certification_details: updates.certificationDetails,
|
||||
owners: updates.owners,
|
||||
roles: updates.roles,
|
||||
tags: updates.tags,
|
||||
});
|
||||
boundActionCreators.setUnsavedChanges(true);
|
||||
boundActionCreators.dashboardTitleChanged(updates.title);
|
||||
|
||||
@@ -383,7 +383,7 @@ const PropertiesModal = ({
|
||||
|
||||
currentJsonMetadata = jsonStringify(metadata);
|
||||
|
||||
const moreOnSubmitProps: { roles?: Roles } = {};
|
||||
const moreOnSubmitProps: { roles?: Roles; tags?: TagType[] } = {};
|
||||
const morePutProps: {
|
||||
roles?: number[];
|
||||
tags?: (string | number | undefined)[];
|
||||
@@ -393,6 +393,7 @@ const PropertiesModal = ({
|
||||
morePutProps.roles = (roles || []).map(r => r.id);
|
||||
}
|
||||
if (isFeatureEnabled(FeatureFlag.TaggingSystem)) {
|
||||
moreOnSubmitProps.tags = tags;
|
||||
morePutProps.tags = tags.map(tag => tag.id);
|
||||
}
|
||||
const onSubmitProps = {
|
||||
@@ -600,6 +601,10 @@ const PropertiesModal = ({
|
||||
setTags(parsedTags);
|
||||
};
|
||||
|
||||
const handleClearTags = () => {
|
||||
setTags([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
@@ -725,6 +730,7 @@ const PropertiesModal = ({
|
||||
value={tagsAsSelectValues}
|
||||
options={loadTags}
|
||||
onChange={handleChangeTags}
|
||||
onClear={handleClearTags}
|
||||
allowClear
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
@@ -173,7 +173,7 @@ test('Should have modal header', async () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Edit Chart Properties')).toBeVisible();
|
||||
expect(screen.getByText('×')).toBeVisible();
|
||||
expect(screen.getByTestId('close-modal-btn')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: 'Close' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 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,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import ColorBreakpointOption from './ColorBreakpointOption';
|
||||
import { ColorBreakpointType, ColorBreakpointOptionProps } from './types';
|
||||
|
||||
const mockBreakpoint: ColorBreakpointType = {
|
||||
id: 0,
|
||||
color: { r: 255, g: 0, b: 0, a: 1 },
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
};
|
||||
|
||||
const mockBreakpoints: ColorBreakpointType[] = [mockBreakpoint];
|
||||
|
||||
const createProps = (): ColorBreakpointOptionProps => ({
|
||||
breakpoint: mockBreakpoint,
|
||||
colorBreakpoints: mockBreakpoints,
|
||||
index: 0,
|
||||
saveColorBreakpoint: jest.fn(),
|
||||
onClose: jest.fn(),
|
||||
onShift: jest.fn(),
|
||||
});
|
||||
|
||||
const renderComponent = (props: Partial<ColorBreakpointOptionProps> = {}) =>
|
||||
render(<ColorBreakpointOption {...createProps()} {...props} />, {
|
||||
useDnd: true,
|
||||
});
|
||||
|
||||
describe('ColorBreakpointOption', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render', async () => {
|
||||
const { container } = renderComponent();
|
||||
await waitFor(() => expect(container).toBeInTheDocument());
|
||||
});
|
||||
|
||||
test('should render the breakpoint range text', async () => {
|
||||
renderComponent();
|
||||
expect(await screen.findByText('0 - 100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the remove button', async () => {
|
||||
renderComponent();
|
||||
const removeBtn = await screen.findByTestId('remove-control-button');
|
||||
expect(removeBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the color preview', async () => {
|
||||
renderComponent();
|
||||
const colorPreview = await screen.findByTestId('color-preview');
|
||||
expect(colorPreview).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call onClose when remove button is clicked', async () => {
|
||||
const onClose = jest.fn();
|
||||
renderComponent({ onClose });
|
||||
|
||||
const removeBtn = await screen.findByTestId('remove-control-button');
|
||||
userEvent.click(removeBtn);
|
||||
|
||||
expect(onClose).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
test('should open popover when clicked', async () => {
|
||||
renderComponent();
|
||||
|
||||
const breakpointOption = await screen.findByTestId(
|
||||
'color-breakpoint-trigger',
|
||||
);
|
||||
userEvent.click(breakpointOption);
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render different color values correctly', async () => {
|
||||
const blueBreakpoint: ColorBreakpointType = {
|
||||
id: 1,
|
||||
color: { r: 0, g: 0, b: 255, a: 1 },
|
||||
minValue: 50,
|
||||
maxValue: 150,
|
||||
};
|
||||
|
||||
renderComponent({ breakpoint: blueBreakpoint });
|
||||
expect(await screen.findByText('50 - 150')).toBeInTheDocument();
|
||||
|
||||
const colorPreview = await screen.findByTestId('color-preview');
|
||||
expect(colorPreview).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle decimal values', async () => {
|
||||
const decimalBreakpoint: ColorBreakpointType = {
|
||||
id: 2,
|
||||
color: { r: 128, g: 128, b: 128, a: 1 },
|
||||
minValue: 0.5,
|
||||
maxValue: 99.9,
|
||||
};
|
||||
|
||||
renderComponent({ breakpoint: decimalBreakpoint });
|
||||
expect(await screen.findByText('0.5 - 99.9')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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 { ColorBreakpointOptionProps } from './types';
|
||||
import ColorBreakpointPopoverTrigger from './ColorBreakpointPopoverTrigger';
|
||||
import { DragContainer } from '../OptionControls';
|
||||
import Option from '../DndColumnSelectControl/Option';
|
||||
|
||||
const BreakpointColorPreview = styled.div`
|
||||
width: ${({ theme }) => theme.sizeUnit * 3}px;
|
||||
height: ${({ theme }) => theme.sizeUnit * 3}px;
|
||||
border-radius: ${({ theme }) => theme.sizeUnit / 2}px;
|
||||
background: ${(props: { color: string }) => props.color};
|
||||
margin-right: ${({ theme }) => theme.sizeUnit}px;
|
||||
`;
|
||||
|
||||
const ColorBreakpointOption = ({
|
||||
breakpoint,
|
||||
colorBreakpoints,
|
||||
index,
|
||||
saveColorBreakpoint,
|
||||
onClose,
|
||||
}: ColorBreakpointOptionProps) => {
|
||||
const { color, minValue, maxValue } = breakpoint;
|
||||
|
||||
const formattedColor = color
|
||||
? `rgba(${color.r}, ${color.g}, ${color.b}, 1)`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<ColorBreakpointPopoverTrigger
|
||||
saveColorBreakpoint={saveColorBreakpoint}
|
||||
value={breakpoint}
|
||||
colorBreakpoints={colorBreakpoints}
|
||||
>
|
||||
<DragContainer data-test="color-breakpoint-trigger">
|
||||
<Option index={index} clickClose={onClose} canDelete withCaret>
|
||||
<BreakpointColorPreview
|
||||
color={formattedColor}
|
||||
data-test="color-preview"
|
||||
/>
|
||||
{`${minValue} - ${maxValue}`}
|
||||
</Option>
|
||||
</DragContainer>
|
||||
</ColorBreakpointPopoverTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorBreakpointOption;
|
||||
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* 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, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ColorBreakpointPopoverControl from './ColorBreakpointPopoverControl';
|
||||
import {
|
||||
ColorBreakpointType,
|
||||
ColorBreakpointsPopoverControlProps,
|
||||
} from './types';
|
||||
|
||||
const mockBreakpoint: ColorBreakpointType = {
|
||||
id: 0,
|
||||
color: { r: 255, g: 0, b: 0, a: 100 },
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
};
|
||||
|
||||
const mockBreakpoints: ColorBreakpointType[] = [mockBreakpoint];
|
||||
|
||||
// Couldn't test the color select - providing color instead of interacting with the color select component
|
||||
const mockEmptyBreakpoint: ColorBreakpointType = {
|
||||
id: 0,
|
||||
color: { r: 0, g: 0, b: 0, a: 0 },
|
||||
};
|
||||
|
||||
const createProps = (): ColorBreakpointsPopoverControlProps => ({
|
||||
value: mockBreakpoint,
|
||||
onSave: jest.fn(),
|
||||
onClose: jest.fn(),
|
||||
colorBreakpoints: mockBreakpoints,
|
||||
});
|
||||
|
||||
const renderComponent = (
|
||||
props: Partial<ColorBreakpointsPopoverControlProps> = {},
|
||||
) => render(<ColorBreakpointPopoverControl {...createProps()} {...props} />);
|
||||
|
||||
describe('ColorBreakpointPopoverControl', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render with default props', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText('Color for breakpoint')).toBeInTheDocument();
|
||||
expect(screen.getByText('Min value')).toBeInTheDocument();
|
||||
expect(screen.getByText('Max value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render close and save buttons', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByTestId('close-button')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('save-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render with existing breakpoint values', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByDisplayValue('0')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call onClose when close button is clicked', async () => {
|
||||
const onClose = jest.fn();
|
||||
renderComponent({ onClose });
|
||||
|
||||
const closeButton = screen.getByTestId('close-button');
|
||||
userEvent.click(closeButton);
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should disable save button when form is incomplete', () => {
|
||||
renderComponent({ value: undefined });
|
||||
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should enable save button when form is complete', async () => {
|
||||
renderComponent({ value: mockEmptyBreakpoint });
|
||||
|
||||
const minInput = screen.getByTestId('min-value-input');
|
||||
const maxInput = screen.getByTestId('max-value-input');
|
||||
|
||||
userEvent.type(minInput, '10');
|
||||
userEvent.type(maxInput, '90');
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should call onSave with correct values when save button is clicked', async () => {
|
||||
const onSave = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
|
||||
renderComponent({
|
||||
onSave,
|
||||
onClose,
|
||||
value: mockEmptyBreakpoint,
|
||||
});
|
||||
|
||||
const minInput = screen.getByTestId('min-value-input');
|
||||
const maxInput = screen.getByTestId('max-value-input');
|
||||
|
||||
userEvent.type(minInput, '10');
|
||||
userEvent.type(maxInput, '90');
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
userEvent.click(saveButton);
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
minValue: 10,
|
||||
maxValue: 90,
|
||||
color: expect.objectContaining({
|
||||
r: expect.any(Number),
|
||||
g: expect.any(Number),
|
||||
b: expect.any(Number),
|
||||
a: expect.any(Number),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should disable save button when min value >= max value', async () => {
|
||||
renderComponent({ value: undefined });
|
||||
|
||||
const minInput = screen.getByTestId('min-value-input');
|
||||
const maxInput = screen.getByTestId('max-value-input');
|
||||
|
||||
userEvent.type(minInput, '100');
|
||||
userEvent.type(maxInput, '50');
|
||||
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should disable save button when breakpoint overlaps with existing ones', async () => {
|
||||
const existingBreakpoints: ColorBreakpointType[] = [
|
||||
{
|
||||
id: 0,
|
||||
color: { r: 255, g: 0, b: 0, a: 100 },
|
||||
minValue: 0,
|
||||
maxValue: 50,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
color: { r: 0, g: 255, b: 0, a: 100 },
|
||||
minValue: 100,
|
||||
maxValue: 200,
|
||||
},
|
||||
];
|
||||
|
||||
renderComponent({
|
||||
colorBreakpoints: existingBreakpoints,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
const minInput = screen.getByTestId('min-value-input');
|
||||
const maxInput = screen.getByTestId('max-value-input');
|
||||
|
||||
userEvent.type(minInput, '25');
|
||||
userEvent.type(maxInput, '75');
|
||||
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should handle non-numeric input validation', async () => {
|
||||
renderComponent({ value: undefined });
|
||||
|
||||
const minInput = screen.getByTestId('min-value-input');
|
||||
const maxInput = screen.getByTestId('max-value-input');
|
||||
|
||||
userEvent.type(minInput, 'abc');
|
||||
userEvent.type(maxInput, '100');
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should update min value when input changes', async () => {
|
||||
renderComponent();
|
||||
|
||||
const minInput = screen.getByDisplayValue('0');
|
||||
userEvent.clear(minInput);
|
||||
userEvent.type(minInput, '20');
|
||||
|
||||
expect(screen.getByDisplayValue('20')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should update max value when input changes', async () => {
|
||||
renderComponent();
|
||||
|
||||
const maxInput = screen.getByDisplayValue('100');
|
||||
userEvent.clear(maxInput);
|
||||
userEvent.type(maxInput, '200');
|
||||
|
||||
expect(screen.getByDisplayValue('200')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle zero values correctly', async () => {
|
||||
renderComponent({
|
||||
value: mockEmptyBreakpoint,
|
||||
});
|
||||
|
||||
const minInput = screen.getByTestId('min-value-input');
|
||||
const maxInput = screen.getByTestId('max-value-input');
|
||||
|
||||
userEvent.type(minInput, '0');
|
||||
userEvent.type(maxInput, '10');
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle negative values correctly', async () => {
|
||||
renderComponent({
|
||||
value: mockEmptyBreakpoint,
|
||||
});
|
||||
|
||||
const minInput = screen.getByTestId('min-value-input');
|
||||
const maxInput = screen.getByTestId('max-value-input');
|
||||
|
||||
userEvent.type(minInput, '-10');
|
||||
userEvent.type(maxInput, '10');
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle decimal values correctly', async () => {
|
||||
renderComponent({
|
||||
value: mockEmptyBreakpoint,
|
||||
});
|
||||
|
||||
const minInput = screen.getByTestId('min-value-input');
|
||||
const maxInput = screen.getByTestId('max-value-input');
|
||||
|
||||
userEvent.type(minInput, '0.5');
|
||||
userEvent.type(maxInput, '99.9');
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should not show overlap error when editing existing breakpoint', async () => {
|
||||
const existingBreakpoints: ColorBreakpointType[] = [
|
||||
{
|
||||
id: 0,
|
||||
color: { r: 255, g: 0, b: 0, a: 100 },
|
||||
minValue: 0,
|
||||
maxValue: 50,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
color: { r: 0, g: 255, b: 0, a: 100 },
|
||||
minValue: 100,
|
||||
maxValue: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const editingBreakpoint = existingBreakpoints[0];
|
||||
renderComponent({
|
||||
colorBreakpoints: existingBreakpoints,
|
||||
value: editingBreakpoint,
|
||||
});
|
||||
|
||||
const minInput = screen.getByDisplayValue('0');
|
||||
userEvent.clear(minInput);
|
||||
userEvent.type(minInput, '10');
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* 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 { useState, useMemo } from 'react';
|
||||
import { Button, Row, Col, InputNumber } from '@superset-ui/core/components';
|
||||
import { styled, t, validateNumber } from '@superset-ui/core';
|
||||
import ControlHeader from '../../ControlHeader';
|
||||
import ColorPickerControl from '../ColorPickerControl';
|
||||
import {
|
||||
ColorBreakpointsPopoverControlProps,
|
||||
ColorType,
|
||||
ColorBreakpointType,
|
||||
ErrorMapType,
|
||||
} from './types';
|
||||
|
||||
const ColorBreakpointActionsContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.sizeUnit * 8}px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const StyledRow = styled(Row)`
|
||||
gap: ${({ theme }) => theme.sizeUnit * 2}px;
|
||||
`;
|
||||
|
||||
const ValuesRow = styled(Row)`
|
||||
gap: ${({ theme }) => theme.sizeUnit * 2}px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
`;
|
||||
|
||||
const NumberControlsDivider = styled.div`
|
||||
padding: 6px 0;
|
||||
`;
|
||||
|
||||
const FullWidthInputNumber = styled(InputNumber)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const determineErrorMap = (
|
||||
colorBreakpoint: ColorBreakpointType,
|
||||
colorBreakpoints: ColorBreakpointType[],
|
||||
) => {
|
||||
const errorMap: ErrorMapType = {
|
||||
minValue: [],
|
||||
maxValue: [],
|
||||
color: [],
|
||||
};
|
||||
const minValueError = validateNumber(colorBreakpoint.minValue);
|
||||
if (minValueError) errorMap.minValue.push(minValueError);
|
||||
|
||||
const maxValueError = validateNumber(colorBreakpoint.maxValue);
|
||||
if (maxValueError) errorMap.maxValue.push(maxValueError);
|
||||
|
||||
if (minValueError || maxValueError) return errorMap;
|
||||
|
||||
const newMinValue = Number(colorBreakpoint.minValue);
|
||||
const newMaxValue = Number(colorBreakpoint.maxValue);
|
||||
|
||||
if (Number.isNaN(newMinValue) || Number.isNaN(newMaxValue)) {
|
||||
return errorMap;
|
||||
}
|
||||
|
||||
if (newMinValue >= newMaxValue) {
|
||||
errorMap.minValue.push(
|
||||
t('Min value should be smaller or equal to max value'),
|
||||
);
|
||||
}
|
||||
|
||||
const otherBreakpoints = colorBreakpoints.filter(
|
||||
breakpoint => breakpoint.id !== colorBreakpoint.id,
|
||||
);
|
||||
|
||||
const isBreakpointDuplicate = !!otherBreakpoints?.find(
|
||||
breakpoint =>
|
||||
Number(breakpoint.minValue) <= newMaxValue &&
|
||||
Number(breakpoint.maxValue) >= newMinValue,
|
||||
);
|
||||
|
||||
if (isBreakpointDuplicate) {
|
||||
const overlapMsg = t('The values overlap other breakpoint values');
|
||||
|
||||
errorMap.minValue.push(overlapMsg);
|
||||
errorMap.maxValue.push(overlapMsg);
|
||||
}
|
||||
|
||||
const validColor =
|
||||
typeof colorBreakpoint.color === 'object' &&
|
||||
'r' in colorBreakpoint.color &&
|
||||
typeof colorBreakpoint.color.r === 'number' &&
|
||||
'g' in colorBreakpoint.color &&
|
||||
typeof colorBreakpoint.color.g === 'number' &&
|
||||
'b' in colorBreakpoint.color &&
|
||||
typeof colorBreakpoint.color.b === 'number' &&
|
||||
'a' in colorBreakpoint.color &&
|
||||
typeof colorBreakpoint.color.a === 'number';
|
||||
|
||||
if (!validColor) {
|
||||
errorMap.color.push(t('Invalid color'));
|
||||
}
|
||||
|
||||
return errorMap;
|
||||
};
|
||||
|
||||
const convertColorBreakpointToNumeric = (
|
||||
colorBreakpoint: ColorBreakpointType,
|
||||
) => {
|
||||
const formattedColorBreakpoint = {
|
||||
color: colorBreakpoint.color,
|
||||
minValue: Number(colorBreakpoint.minValue),
|
||||
maxValue: Number(colorBreakpoint.maxValue),
|
||||
};
|
||||
return formattedColorBreakpoint;
|
||||
};
|
||||
|
||||
const DEFAULT_COLOR_BREAKPOINT: ColorBreakpointType = {
|
||||
id: undefined,
|
||||
minValue: undefined,
|
||||
maxValue: undefined,
|
||||
color: { r: 0, g: 0, b: 0, a: 100 },
|
||||
};
|
||||
|
||||
const ColorBreakpointsPopoverControl = ({
|
||||
value: initialValue,
|
||||
onSave,
|
||||
onClose,
|
||||
colorBreakpoints,
|
||||
}: ColorBreakpointsPopoverControlProps) => {
|
||||
const [colorBreakpoint, setColorBreakpoint] = useState(
|
||||
initialValue || DEFAULT_COLOR_BREAKPOINT,
|
||||
);
|
||||
|
||||
const useValidationErrors = (
|
||||
colorBreakpoint: ColorBreakpointType,
|
||||
colorBreakpoints: ColorBreakpointType[],
|
||||
) =>
|
||||
useMemo(
|
||||
() => determineErrorMap(colorBreakpoint, colorBreakpoints),
|
||||
[colorBreakpoint, colorBreakpoints],
|
||||
);
|
||||
|
||||
const validationErrors = useValidationErrors(
|
||||
colorBreakpoint,
|
||||
colorBreakpoints,
|
||||
);
|
||||
|
||||
const updateColor = (rgb: ColorType) => {
|
||||
setColorBreakpoint({
|
||||
...colorBreakpoint,
|
||||
color: { ...rgb },
|
||||
});
|
||||
};
|
||||
|
||||
const updateMinValue = (value: number) => {
|
||||
setColorBreakpoint({
|
||||
...colorBreakpoint,
|
||||
minValue: value,
|
||||
});
|
||||
};
|
||||
|
||||
const updateMaxValue = (value: number) => {
|
||||
setColorBreakpoint({
|
||||
...colorBreakpoint,
|
||||
maxValue: value,
|
||||
});
|
||||
};
|
||||
|
||||
const containsErrors = Object.values(validationErrors).some(
|
||||
errors => errors.length > 0,
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!containsErrors && onSave) {
|
||||
onSave(convertColorBreakpointToNumeric(colorBreakpoint));
|
||||
if (onClose) onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div role="dialog">
|
||||
<StyledRow>
|
||||
<Col flex="1">
|
||||
<ControlHeader
|
||||
name="color"
|
||||
label={t('Color for breakpoint')}
|
||||
validationErrors={validationErrors.color}
|
||||
hovered
|
||||
/>
|
||||
<ColorPickerControl
|
||||
value={colorBreakpoint.color}
|
||||
onChange={updateColor}
|
||||
data-test="color-picker"
|
||||
/>
|
||||
</Col>
|
||||
</StyledRow>
|
||||
<ValuesRow>
|
||||
<Col flex="1">
|
||||
<ControlHeader
|
||||
name="min-value"
|
||||
label={t('Min value')}
|
||||
validationErrors={validationErrors.minValue}
|
||||
hovered
|
||||
/>
|
||||
<FullWidthInputNumber
|
||||
value={colorBreakpoint.minValue}
|
||||
onChange={updateMinValue}
|
||||
data-test="min-value-input"
|
||||
/>
|
||||
</Col>
|
||||
<NumberControlsDivider>-</NumberControlsDivider>
|
||||
<Col flex="1">
|
||||
<ControlHeader
|
||||
name="max-value"
|
||||
label={t('Max value')}
|
||||
validationErrors={validationErrors.maxValue}
|
||||
hovered
|
||||
/>
|
||||
<FullWidthInputNumber
|
||||
value={colorBreakpoint.maxValue}
|
||||
onChange={updateMaxValue}
|
||||
data-test="max-value-input"
|
||||
/>
|
||||
</Col>
|
||||
</ValuesRow>
|
||||
<ColorBreakpointActionsContainer>
|
||||
<Button
|
||||
buttonSize="small"
|
||||
buttonStyle="secondary"
|
||||
onClick={onClose}
|
||||
aria-label={t('Close color breakpoint editor')}
|
||||
data-test="close-button"
|
||||
>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={containsErrors}
|
||||
buttonStyle="primary"
|
||||
buttonSize="small"
|
||||
onClick={handleSave}
|
||||
aria-label={t('Save color breakpoint values')}
|
||||
data-test="save-button"
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</ColorBreakpointActionsContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorBreakpointsPopoverControl;
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 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 ColorBreakpointPopoverTrigger from './ColorBreakpointPopoverTrigger';
|
||||
import {
|
||||
ColorBreakpointType,
|
||||
ColorBreakpointsPopoverTriggerProps,
|
||||
} from './types';
|
||||
|
||||
const mockBreakpoint: ColorBreakpointType = {
|
||||
id: 0,
|
||||
color: { r: 255, g: 0, b: 0, a: 1 },
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
};
|
||||
|
||||
// Couldn't test the color select - providing color instead of interacting with the color select component
|
||||
const mockEmptyBreakpoint: ColorBreakpointType = {
|
||||
id: 0,
|
||||
color: { r: 0, g: 0, b: 0, a: 0 },
|
||||
};
|
||||
|
||||
const mockBreakpoints: ColorBreakpointType[] = [mockBreakpoint];
|
||||
|
||||
const createProps = (): ColorBreakpointsPopoverTriggerProps => ({
|
||||
value: mockBreakpoint,
|
||||
saveColorBreakpoint: jest.fn(),
|
||||
colorBreakpoints: mockBreakpoints,
|
||||
});
|
||||
|
||||
const renderComponent = (
|
||||
props: Partial<ColorBreakpointsPopoverTriggerProps> = {},
|
||||
) =>
|
||||
render(
|
||||
<ColorBreakpointPopoverTrigger {...createProps()} {...props}>
|
||||
Click to add new breakpoint
|
||||
</ColorBreakpointPopoverTrigger>,
|
||||
);
|
||||
|
||||
describe('ColorBreakpointPopoverTrigger', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render', () => {
|
||||
const { container } = renderComponent();
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render children', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('Click to add new breakpoint')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the popover on click when uncontrolled', async () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
|
||||
const triggerButton = screen.getByText('Click to add new breakpoint');
|
||||
userEvent.click(triggerButton);
|
||||
|
||||
expect(screen.queryByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should be visible when controlled and visible is true', async () => {
|
||||
const controlledProps = {
|
||||
isControlled: true,
|
||||
visible: true,
|
||||
toggleVisibility: jest.fn(),
|
||||
};
|
||||
|
||||
renderComponent(controlledProps);
|
||||
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT be visible when controlled and visible is false', () => {
|
||||
const controlledProps = {
|
||||
isControlled: true,
|
||||
visible: false,
|
||||
toggleVisibility: jest.fn(),
|
||||
};
|
||||
|
||||
renderComponent(controlledProps);
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call toggleVisibility when controlled and popover state changes', async () => {
|
||||
const toggleVisibility = jest.fn();
|
||||
const controlledProps = {
|
||||
isControlled: true,
|
||||
visible: false,
|
||||
toggleVisibility,
|
||||
};
|
||||
|
||||
renderComponent(controlledProps);
|
||||
|
||||
const triggerButton = screen.getByText('Click to add new breakpoint');
|
||||
await userEvent.click(triggerButton);
|
||||
|
||||
expect(toggleVisibility).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('should render popover content with form elements', async () => {
|
||||
renderComponent();
|
||||
|
||||
const triggerButton = screen.getByText('Click to add new breakpoint');
|
||||
userEvent.click(triggerButton);
|
||||
|
||||
expect(screen.getByText('Color for breakpoint')).toBeInTheDocument();
|
||||
expect(screen.getByText('Min value')).toBeInTheDocument();
|
||||
expect(screen.getByText('Max value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should close popover when save is called', async () => {
|
||||
renderComponent({ value: mockEmptyBreakpoint });
|
||||
|
||||
const triggerButton = screen.getByText('Click to add new breakpoint');
|
||||
userEvent.click(triggerButton);
|
||||
|
||||
const minInput = screen.getAllByRole('spinbutton')[0];
|
||||
const maxInput = screen.getAllByRole('spinbutton')[1];
|
||||
|
||||
userEvent.type(minInput, '10');
|
||||
userEvent.type(maxInput, '90');
|
||||
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
|
||||
expect(saveButton).toBeEnabled();
|
||||
userEvent.click(saveButton);
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should close popover when close button is clicked', async () => {
|
||||
renderComponent();
|
||||
|
||||
const triggerButton = screen.getByText('Click to add new breakpoint');
|
||||
userEvent.click(triggerButton);
|
||||
|
||||
const closeButton = screen.getByTestId('close-button');
|
||||
userEvent.click(closeButton);
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle undefined value prop', async () => {
|
||||
renderComponent({ value: undefined });
|
||||
|
||||
const triggerButton = screen.getByText('Click to add new breakpoint');
|
||||
userEvent.click(triggerButton);
|
||||
|
||||
expect(screen.queryByRole('dialog')).toBeInTheDocument();
|
||||
expect(screen.getByText('Color for breakpoint')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle popover open state changes correctly', async () => {
|
||||
renderComponent();
|
||||
|
||||
const triggerButton = screen.getByText('Click to add new breakpoint');
|
||||
|
||||
userEvent.click(triggerButton);
|
||||
expect(screen.queryByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
userEvent.click(triggerButton);
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should maintain controlled state when toggleVisibility is provided', async () => {
|
||||
const toggleVisibility = jest.fn();
|
||||
const controlledProps = {
|
||||
isControlled: true,
|
||||
visible: true,
|
||||
toggleVisibility,
|
||||
};
|
||||
|
||||
renderComponent(controlledProps);
|
||||
|
||||
expect(screen.queryByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
const closeButton = screen.getByTestId('close-button');
|
||||
userEvent.click(closeButton);
|
||||
|
||||
expect(toggleVisibility).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test('should pass colorBreakpoints to popover content', async () => {
|
||||
const colorBreakpoints = [
|
||||
{ id: 0, color: { r: 255, g: 0, b: 0, a: 1 }, minValue: 0, maxValue: 50 },
|
||||
{
|
||||
id: 1,
|
||||
color: { r: 0, g: 255, b: 0, a: 1 },
|
||||
minValue: 50,
|
||||
maxValue: 100,
|
||||
},
|
||||
];
|
||||
|
||||
renderComponent({ colorBreakpoints });
|
||||
|
||||
const triggerButton = screen.getByText('Click to add new breakpoint');
|
||||
userEvent.click(triggerButton);
|
||||
|
||||
expect(screen.queryByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle destroyOnHidden prop', async () => {
|
||||
renderComponent();
|
||||
|
||||
const triggerButton = screen.getByText('Click to add new breakpoint');
|
||||
userEvent.click(triggerButton);
|
||||
|
||||
expect(screen.queryByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
const closeButton = screen.getByTestId('close-button');
|
||||
userEvent.click(closeButton);
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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 { useState } from 'react';
|
||||
import ControlPopover from '../ControlPopover/ControlPopover';
|
||||
import { ColorBreakpointsPopoverTriggerProps } from './types';
|
||||
import ColorBreakpointPopoverControl from './ColorBreakpointPopoverControl';
|
||||
|
||||
const ColorBreakpointsPopoverTrigger = ({
|
||||
value: initialValue,
|
||||
saveColorBreakpoint,
|
||||
isControlled,
|
||||
visible: controlledVisibility,
|
||||
toggleVisibility,
|
||||
colorBreakpoints,
|
||||
...props
|
||||
}: ColorBreakpointsPopoverTriggerProps) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const visible = isControlled ? controlledVisibility : isVisible;
|
||||
const setVisibility =
|
||||
isControlled && toggleVisibility ? toggleVisibility : setIsVisible;
|
||||
|
||||
const popoverContent = (
|
||||
<ColorBreakpointPopoverControl
|
||||
value={initialValue}
|
||||
colorBreakpoints={colorBreakpoints}
|
||||
onSave={saveColorBreakpoint}
|
||||
onClose={() => setVisibility(false)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ControlPopover
|
||||
trigger="click"
|
||||
content={popoverContent}
|
||||
defaultOpen={visible}
|
||||
open={visible}
|
||||
onOpenChange={setVisibility}
|
||||
destroyOnHidden
|
||||
>
|
||||
{props.children}
|
||||
</ControlPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorBreakpointsPopoverTrigger;
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 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, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ColorBreakpointsControl from '.';
|
||||
import { ColorBreakpointType, ColorBreakpointsControlProps } from './types';
|
||||
|
||||
interface Props extends ColorBreakpointsControlProps {
|
||||
name: string;
|
||||
label: string;
|
||||
value: ColorBreakpointType[];
|
||||
onChange: jest.Mock;
|
||||
breakpoints: ColorBreakpointType[];
|
||||
}
|
||||
|
||||
const createProps = (): Props => ({
|
||||
name: 'ColorBreakpointsControl',
|
||||
label: 'Color Breakpoints',
|
||||
value: [],
|
||||
onChange: jest.fn(),
|
||||
breakpoints: [],
|
||||
actions: {
|
||||
setControlValue: jest.fn(),
|
||||
},
|
||||
type: 'ColorBreakpointsControl',
|
||||
});
|
||||
|
||||
const renderComponent = (props: Partial<Props> = {}) =>
|
||||
render(<ColorBreakpointsControl {...createProps()} {...props} />, {
|
||||
useDnd: true,
|
||||
});
|
||||
|
||||
describe('ColorBreakpointsControl', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render with default props', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('Click to add new breakpoint')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render existing breakpoints', () => {
|
||||
const existingBreakpoint: ColorBreakpointType = {
|
||||
id: 0,
|
||||
color: { r: 255, g: 0, b: 0, a: 1 },
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
};
|
||||
|
||||
renderComponent({ value: [existingBreakpoint] });
|
||||
expect(screen.getByText('0 - 100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle empty breakpoints array', () => {
|
||||
renderComponent({ value: [] });
|
||||
expect(screen.getByText('Click to add new breakpoint')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle multiple breakpoints', () => {
|
||||
const breakpoints: ColorBreakpointType[] = [
|
||||
{
|
||||
id: 0,
|
||||
color: { r: 255, g: 0, b: 0, a: 1 },
|
||||
minValue: 0,
|
||||
maxValue: 50,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
color: { r: 0, g: 255, b: 0, a: 1 },
|
||||
minValue: 50,
|
||||
maxValue: 100,
|
||||
},
|
||||
];
|
||||
|
||||
renderComponent({ value: breakpoints });
|
||||
expect(screen.getByText('0 - 50')).toBeInTheDocument();
|
||||
expect(screen.getByText('50 - 100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call onChange when component state updates', () => {
|
||||
const onChange = jest.fn();
|
||||
renderComponent({ onChange });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
test('should show new breakpoint button when no breakpoints exist', () => {
|
||||
renderComponent();
|
||||
const ghostButton = screen.getByText('Click to add new breakpoint');
|
||||
expect(ghostButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle new breakpoint button click and popover visibility state', async () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
|
||||
const addButton = screen.getByText('Click to add new breakpoint');
|
||||
userEvent.click(addButton);
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should save new breakpoint and update state', async () => {
|
||||
const onChange = jest.fn();
|
||||
renderComponent({ onChange });
|
||||
|
||||
const addButton = screen.getByText('Click to add new breakpoint');
|
||||
userEvent.click(addButton);
|
||||
|
||||
const minInput = screen.getByTestId('min-value-input');
|
||||
const maxInput = screen.getByTestId('max-value-input');
|
||||
|
||||
userEvent.type(minInput, '10');
|
||||
userEvent.type(maxInput, '90');
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
userEvent.click(saveButton);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
minValue: 10,
|
||||
maxValue: 90,
|
||||
id: 0,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should remove breakpoint when delete is triggered', async () => {
|
||||
const existingBreakpoint: ColorBreakpointType = {
|
||||
id: 0,
|
||||
color: { r: 255, g: 0, b: 0, a: 1 },
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
};
|
||||
const onChange = jest.fn();
|
||||
|
||||
renderComponent({ value: [existingBreakpoint], onChange });
|
||||
|
||||
const removeButton = screen.getByTestId('remove-control-button');
|
||||
userEvent.click(removeButton);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
test('should edit existing breakpoint when clicked', async () => {
|
||||
const existingBreakpoint: ColorBreakpointType = {
|
||||
id: 0,
|
||||
color: { r: 255, g: 0, b: 0, a: 1 },
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
};
|
||||
const onChange = jest.fn();
|
||||
|
||||
renderComponent({ value: [existingBreakpoint], onChange });
|
||||
|
||||
const breakpointOption = screen.getByText('0 - 100');
|
||||
userEvent.click(breakpointOption);
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('0')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle DndSelectLabel props correctly', () => {
|
||||
renderComponent();
|
||||
|
||||
const dndSelectLabel = screen
|
||||
.getByText('Click to add new breakpoint')
|
||||
.closest('div');
|
||||
expect(dndSelectLabel).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should assign incremental IDs to new breakpoints', async () => {
|
||||
const onChange = jest.fn();
|
||||
renderComponent({ onChange });
|
||||
|
||||
const addButton = screen.getByText('Click to add new breakpoint');
|
||||
userEvent.click(addButton);
|
||||
|
||||
const minInput = screen.getByTestId('min-value-input');
|
||||
const maxInput = screen.getByTestId('max-value-input');
|
||||
|
||||
userEvent.type(minInput, '0');
|
||||
userEvent.type(maxInput, '50');
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByTestId('save-button');
|
||||
userEvent.click(saveButton);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([expect.objectContaining({ id: 0 })]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
|
||||
import ColorBreakpointOption from './ColorBreakpointOption';
|
||||
import { ColorBreakpointType, ColorBreakpointsControlProps } from './types';
|
||||
import ColorBreakpointPopoverTrigger from './ColorBreakpointPopoverTrigger';
|
||||
|
||||
const DEFAULT_COLOR_BREAKPOINTS: ColorBreakpointType[] = [];
|
||||
|
||||
const NewColorBreakpointFormatPlaceholder = styled('div')`
|
||||
position: relative;
|
||||
width: calc(100% - ${({ theme }) => theme.sizeUnit}px);
|
||||
bottom: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
left: 0;
|
||||
`;
|
||||
|
||||
const ColorBreakpointsControl = ({
|
||||
onChange,
|
||||
...props
|
||||
}: ColorBreakpointsControlProps) => {
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
const [colorBreakpoints, setColorBreakpoints] = useState<
|
||||
ColorBreakpointType[]
|
||||
>(props?.value ? props?.value : DEFAULT_COLOR_BREAKPOINTS);
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.(colorBreakpoints);
|
||||
}, [colorBreakpoints, onChange]);
|
||||
|
||||
const togglePopover = (visible: boolean) => {
|
||||
setPopoverVisible(visible);
|
||||
};
|
||||
|
||||
const handleClickGhostButton = () => {
|
||||
togglePopover(true);
|
||||
};
|
||||
|
||||
const saveColorBreakpoint = (breakpoint: ColorBreakpointType) => {
|
||||
setColorBreakpoints([
|
||||
...colorBreakpoints,
|
||||
{
|
||||
...breakpoint,
|
||||
id: colorBreakpoints.length,
|
||||
},
|
||||
]);
|
||||
togglePopover(false);
|
||||
};
|
||||
|
||||
const removeColorBreakpoint = (index: number) => {
|
||||
const newBreakpoints = [...colorBreakpoints];
|
||||
newBreakpoints.splice(index, 1);
|
||||
setColorBreakpoints(newBreakpoints);
|
||||
};
|
||||
|
||||
const editColorBreakpoint = (
|
||||
breakpoint: ColorBreakpointType,
|
||||
index: number,
|
||||
) => {
|
||||
const newBreakpoints = [...colorBreakpoints];
|
||||
newBreakpoints[index] = {
|
||||
...breakpoint,
|
||||
id: index,
|
||||
};
|
||||
setColorBreakpoints(newBreakpoints);
|
||||
};
|
||||
|
||||
const valuesRenderer = () =>
|
||||
colorBreakpoints.map((breakpoint, index) => (
|
||||
<ColorBreakpointOption
|
||||
key={index}
|
||||
saveColorBreakpoint={(newBreakpoint: ColorBreakpointType) =>
|
||||
editColorBreakpoint(newBreakpoint, index)
|
||||
}
|
||||
breakpoint={breakpoint}
|
||||
colorBreakpoints={colorBreakpoints}
|
||||
index={index}
|
||||
onClose={removeColorBreakpoint}
|
||||
onShift={() => {}}
|
||||
/>
|
||||
));
|
||||
|
||||
const ghostButtonText = t('Click to add new breakpoint');
|
||||
|
||||
return (
|
||||
<>
|
||||
<DndSelectLabel
|
||||
onDrop={() => {}}
|
||||
canDrop={() => false}
|
||||
valuesRenderer={valuesRenderer}
|
||||
accept={[]}
|
||||
ghostButtonText={ghostButtonText}
|
||||
onClickGhostButton={handleClickGhostButton}
|
||||
{...props}
|
||||
/>
|
||||
<ColorBreakpointPopoverTrigger
|
||||
saveColorBreakpoint={saveColorBreakpoint}
|
||||
colorBreakpoints={colorBreakpoints}
|
||||
isControlled
|
||||
visible={popoverVisible}
|
||||
toggleVisibility={setPopoverVisible}
|
||||
>
|
||||
<NewColorBreakpointFormatPlaceholder />
|
||||
</ColorBreakpointPopoverTrigger>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorBreakpointsControl;
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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 { ReactNode } from 'react';
|
||||
import { OptionValueType } from 'src/explore/components/controls/DndColumnSelectControl/types';
|
||||
import { ControlComponentProps } from 'src/explore/components/Control';
|
||||
|
||||
export interface ColorType {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
export interface ColorBreakpointType {
|
||||
id?: number;
|
||||
color?: ColorType;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
}
|
||||
|
||||
export interface ErrorMapType {
|
||||
color: string[];
|
||||
minValue: string[];
|
||||
maxValue: string[];
|
||||
}
|
||||
|
||||
export interface ColorBreakpointsControlProps
|
||||
extends ControlComponentProps<OptionValueType[]> {
|
||||
breakpoints: ColorBreakpointType[];
|
||||
}
|
||||
|
||||
export interface ColorBreakpointsPopoverTriggerProps {
|
||||
description?: string;
|
||||
hovered?: boolean;
|
||||
value?: ColorBreakpointType;
|
||||
children?: ReactNode;
|
||||
saveColorBreakpoint: (colorBreakpoint: ColorBreakpointType) => void;
|
||||
isControlled?: boolean;
|
||||
visible?: boolean;
|
||||
toggleVisibility?: (visibility: boolean) => void;
|
||||
colorBreakpoints: ColorBreakpointType[];
|
||||
}
|
||||
|
||||
export interface ColorBreakpointsPopoverControlProps {
|
||||
description?: string;
|
||||
hovered?: boolean;
|
||||
value?: ColorBreakpointType;
|
||||
onSave?: (colorBreakpoint: ColorBreakpointType) => void;
|
||||
onClose?: () => void;
|
||||
colorBreakpoints: ColorBreakpointType[];
|
||||
}
|
||||
|
||||
export interface ColorBreakpointOptionProps {
|
||||
breakpoint: ColorBreakpointType;
|
||||
colorBreakpoints: ColorBreakpointType[];
|
||||
index: number;
|
||||
saveColorBreakpoint: (colorBreakpoint: ColorBreakpointType) => void;
|
||||
onClose: (index: number) => void;
|
||||
onShift: (hoverIndex: number, dragIndex: number) => void;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ const ContourOption = ({
|
||||
|
||||
const formattedColor = color
|
||||
? `rgba(${color.r}, ${color.g}, ${color.b}, 1)`
|
||||
: 'undefined';
|
||||
: 'rgba(0,0,0,0)';
|
||||
|
||||
const formatIsoline = (threshold: number, width: number) =>
|
||||
`${t('Threshold')}: ${threshold}, ${t('color')}: ${formattedColor}, ${t(
|
||||
|
||||
@@ -479,3 +479,30 @@ test('should show missing dataset state', () => {
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show forbidden dataset state', () => {
|
||||
// @ts-ignore
|
||||
delete window.location;
|
||||
// @ts-ignore
|
||||
window.location = { search: '?slice_id=152' };
|
||||
const error = {
|
||||
error_type: 'TABLE_SECURITY_ACCESS_ERROR',
|
||||
statusText: 'FORBIDDEN',
|
||||
message: 'You do not have access to the following tables: blocked_table',
|
||||
extra: {
|
||||
datasource: 152,
|
||||
datasource_name: 'forbidden dataset',
|
||||
},
|
||||
};
|
||||
const props = createProps({
|
||||
datasource: {
|
||||
...fallbackExploreInitialData.dataset,
|
||||
extra: {
|
||||
error,
|
||||
},
|
||||
},
|
||||
});
|
||||
render(<DatasourceControl {...props} />, { useRedux: true, useRouter: true });
|
||||
expect(screen.getByText(error.message)).toBeInTheDocument();
|
||||
expect(screen.getByText(error.statusText)).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
userHasPermission,
|
||||
isUserAdmin,
|
||||
} from 'src/dashboard/util/permissionUtils';
|
||||
import { ErrorMessageWithStackTrace } from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
|
||||
import ViewQueryModalFooter from 'src/explore/components/controls/ViewQueryModalFooter';
|
||||
import ViewQuery from 'src/explore/components/controls/ViewQuery';
|
||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
@@ -281,7 +282,17 @@ class DatasourceControl extends PureComponent {
|
||||
showSaveDatasetModal,
|
||||
} = this.state;
|
||||
const { datasource, onChange, theme } = this.props;
|
||||
const isMissingDatasource = !datasource?.id;
|
||||
let extra;
|
||||
if (datasource?.extra) {
|
||||
if (typeof datasource.extra === 'string') {
|
||||
try {
|
||||
extra = JSON.parse(datasource.extra);
|
||||
} catch {} // eslint-disable-line no-empty
|
||||
} else {
|
||||
extra = datasource.extra; // eslint-disable-line prefer-destructuring
|
||||
}
|
||||
}
|
||||
const isMissingDatasource = !datasource?.id || Boolean(extra?.error);
|
||||
let isMissingParams = false;
|
||||
if (isMissingDatasource) {
|
||||
const datasourceId = getUrlParam(URL_PARAMS.datasourceId);
|
||||
@@ -386,20 +397,10 @@ class DatasourceControl extends PureComponent {
|
||||
|
||||
const { health_check_message: healthCheckMessage } = datasource;
|
||||
|
||||
let extra;
|
||||
if (datasource?.extra) {
|
||||
if (typeof datasource.extra === 'string') {
|
||||
try {
|
||||
extra = JSON.parse(datasource.extra);
|
||||
} catch {} // eslint-disable-line no-empty
|
||||
} else {
|
||||
extra = datasource.extra; // eslint-disable-line prefer-destructuring
|
||||
}
|
||||
}
|
||||
|
||||
const titleText = isMissingDatasource
|
||||
? t('Missing dataset')
|
||||
: getDatasourceTitle(datasource);
|
||||
const titleText =
|
||||
isMissingDatasource && !datasource.name
|
||||
? t('Missing dataset')
|
||||
: getDatasourceTitle(datasource);
|
||||
|
||||
const tooltip = titleText;
|
||||
|
||||
@@ -452,31 +453,44 @@ class DatasourceControl extends PureComponent {
|
||||
)}
|
||||
{isMissingDatasource && !isMissingParams && (
|
||||
<div className="error-alert">
|
||||
<ErrorAlert
|
||||
type="warning"
|
||||
errorType={t('Missing dataset')}
|
||||
descriptionPre={false}
|
||||
descriptionDetailsCollapsed={false}
|
||||
descriptionDetails={
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
'The dataset linked to this chart may have been deleted.',
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<Button
|
||||
buttonStyle="warning"
|
||||
onClick={() =>
|
||||
this.handleMenuItemClick({ key: CHANGE_DATASET })
|
||||
}
|
||||
>
|
||||
{t('Swap dataset')}
|
||||
</Button>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{extra?.error ? (
|
||||
<div className="error-alert">
|
||||
<ErrorMessageWithStackTrace
|
||||
title={extra.error.statusText || extra.error.message}
|
||||
subtitle={
|
||||
extra.error.statusText ? extra.error.message : undefined
|
||||
}
|
||||
error={extra.error}
|
||||
source="explore"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ErrorAlert
|
||||
type="warning"
|
||||
errorType={t('Missing dataset')}
|
||||
descriptionPre={false}
|
||||
descriptionDetailsCollapsed={false}
|
||||
descriptionDetails={
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
'The dataset linked to this chart may have been deleted.',
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<Button
|
||||
buttonStyle="warning"
|
||||
onClick={() =>
|
||||
this.handleMenuItemClick({ key: CHANGE_DATASET })
|
||||
}
|
||||
>
|
||||
{t('Swap dataset')}
|
||||
</Button>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showEditDatasourceModal && (
|
||||
|
||||
@@ -90,7 +90,10 @@ const DndFilterSelect = (props: DndFilterSelectProps) => {
|
||||
let extra = {};
|
||||
if (datasource?.extra) {
|
||||
try {
|
||||
extra = JSON.parse(datasource.extra);
|
||||
extra =
|
||||
typeof datasource.extra === 'string'
|
||||
? JSON.parse(datasource.extra)
|
||||
: datasource.extra;
|
||||
} catch {} // eslint-disable-line no-empty
|
||||
}
|
||||
return extra;
|
||||
|
||||
@@ -56,6 +56,7 @@ import MapViewControl from './MapViewControl/MapViewControl';
|
||||
import ZoomConfigControl from './ZoomConfigControl/ZoomConfigControl';
|
||||
import NumberControl from './NumberControl';
|
||||
import TimeRangeControl from './TimeRangeControl';
|
||||
import ColorBreakpointsControl from './ColorBreakpointsControl';
|
||||
|
||||
const extensionsRegistry = getExtensionsRegistry();
|
||||
const DateFilterControlExtension = extensionsRegistry.get(
|
||||
@@ -79,6 +80,7 @@ const controlMap = {
|
||||
DndFilterSelect,
|
||||
DndMetricSelect,
|
||||
FixedOrMetricControl,
|
||||
ColorBreakpointsControl,
|
||||
HiddenControl,
|
||||
LayerConfigsControl,
|
||||
MapViewControl,
|
||||
|
||||
@@ -159,8 +159,8 @@ export const fallbackExploreInitialData: ExplorePageInitialData = {
|
||||
verbose_map: {},
|
||||
main_dttm_col: '',
|
||||
owners: [],
|
||||
datasource_name: 'missing_datasource',
|
||||
name: 'missing_datasource',
|
||||
datasource_name: '',
|
||||
name: '',
|
||||
description: null,
|
||||
},
|
||||
slice: null,
|
||||
|
||||
@@ -69,7 +69,7 @@ export type Datasource = Dataset & {
|
||||
catalog?: string | null;
|
||||
schema?: string;
|
||||
is_sqllab_view?: boolean;
|
||||
extra?: string;
|
||||
extra?: string | object;
|
||||
};
|
||||
|
||||
export interface ExplorePageInitialData {
|
||||
|
||||
@@ -252,8 +252,9 @@ export type DBReducerActionType =
|
||||
};
|
||||
|
||||
const StyledBtns = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.sizeUnit * 3}px;
|
||||
margin-left: ${({ theme }) => theme.sizeUnit * 3}px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.sizeUnit * 5}px;
|
||||
`;
|
||||
|
||||
export function dbReducer(
|
||||
@@ -702,6 +703,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
|
||||
// Test Connection logic
|
||||
const testConnection = () => {
|
||||
handleClearValidationErrors();
|
||||
if (!db?.sqlalchemy_uri) {
|
||||
addDangerToast(t('Please enter a SQLAlchemy URI to test'));
|
||||
return;
|
||||
@@ -728,10 +730,12 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
(errorMsg: string) => {
|
||||
setTestInProgress(false);
|
||||
addDangerToast(errorMsg);
|
||||
setHasValidated(false);
|
||||
},
|
||||
(errorMsg: string) => {
|
||||
setTestInProgress(false);
|
||||
addSuccessToast(errorMsg);
|
||||
setHasValidated(true);
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -761,6 +765,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
const handleClearValidationErrors = useCallback(() => {
|
||||
setValidationErrors(null);
|
||||
setHasValidated(false);
|
||||
clearError();
|
||||
}, [setValidationErrors, setHasValidated]);
|
||||
|
||||
const handleParametersChange = useCallback(
|
||||
@@ -820,7 +825,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
const onSave = async () => {
|
||||
let dbConfigExtraExtensionOnSaveError;
|
||||
setLoading(true);
|
||||
|
||||
setHasValidated(false);
|
||||
dbConfigExtraExtension
|
||||
?.onSave(extraExtensionComponentState, db)
|
||||
.then(({ error }: { error: any }) => {
|
||||
@@ -1166,6 +1171,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
};
|
||||
|
||||
const handleBackButtonOnConnect = () => {
|
||||
handleClearValidationErrors();
|
||||
if (editNewDb) setHasConnectedDb(false);
|
||||
if (importingModal) setImportingModal(false);
|
||||
if (importErrored) {
|
||||
@@ -1925,14 +1931,15 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
target,
|
||||
}: {
|
||||
target: HTMLInputElement;
|
||||
}) =>
|
||||
}) => {
|
||||
setHasValidated(false);
|
||||
onChange(ActionType.InputChange, {
|
||||
type: target.type,
|
||||
name: target.name,
|
||||
checked: target.checked,
|
||||
value: target.value,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
conf={conf}
|
||||
testConnection={testConnection}
|
||||
testInProgress={testInProgress}
|
||||
@@ -2156,7 +2163,8 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
<Button
|
||||
data-test="sqla-connect-btn"
|
||||
buttonStyle="link"
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
handleClearValidationErrors();
|
||||
setDB({
|
||||
type: ActionType.ConfigMethodChange,
|
||||
payload: {
|
||||
@@ -2165,8 +2173,8 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
ConfigurationMethod.SqlalchemyUri,
|
||||
database_name: db.database_name,
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
css={buttonLinkStyles}
|
||||
>
|
||||
{t(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user