mirror of
https://github.com/apache/superset.git
synced 2026-05-07 00:44:26 +00:00
Compare commits
88 Commits
plavacquer
...
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 | ||
|
|
0d236c4ade | ||
|
|
bc0a10fc73 | ||
|
|
5efca408eb | ||
|
|
29f638e239 | ||
|
|
ddeb612429 | ||
|
|
0bc214e889 | ||
|
|
d951158ce6 | ||
|
|
85034b9748 | ||
|
|
11215b092a | ||
|
|
2129e22423 | ||
|
|
7ea1fca4f7 | ||
|
|
9c8fdc0fc1 | ||
|
|
e25be0f3d9 | ||
|
|
d633fe47ef | ||
|
|
c25e734407 | ||
|
|
d8fd6de940 | ||
|
|
733f112142 | ||
|
|
f55476034b | ||
|
|
0a5941edd7 | ||
|
|
0fc4119728 | ||
|
|
6adfd33e3a | ||
|
|
829e4d92d9 | ||
|
|
42db43c686 | ||
|
|
a0f9efd45e | ||
|
|
5d6a979cd0 | ||
|
|
d6ed819fe2 | ||
|
|
ef14a5fbb4 | ||
|
|
4718767ddb | ||
|
|
dfb377c636 | ||
|
|
7082933b96 | ||
|
|
11b6263d55 | ||
|
|
b0cf7b61ad | ||
|
|
96a1b33f22 | ||
|
|
5cff87c048 | ||
|
|
b9052fa461 | ||
|
|
0ea2066d5b | ||
|
|
992aa3a4d5 | ||
|
|
36c7b15342 | ||
|
|
2ab85f3b67 | ||
|
|
1b690a9876 | ||
|
|
f18d9b6bf4 | ||
|
|
ebca5169a0 | ||
|
|
b9ba4d6fda | ||
|
|
20371940d3 | ||
|
|
ee4944bc1a | ||
|
|
5a1023da89 | ||
|
|
4b94d25869 | ||
|
|
2ceced71c5 |
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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
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
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.5.2",
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@docusaurus/core": "3.8.1",
|
||||
"@docusaurus/plugin-client-redirects": "3.8.1",
|
||||
"@docusaurus/preset-classic": "3.8.1",
|
||||
|
||||
@@ -165,6 +165,13 @@
|
||||
dependencies:
|
||||
"@ant-design/fast-color" "^2.0.6"
|
||||
|
||||
"@ant-design/colors@^8.0.0":
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-8.0.0.tgz#92b5aa1cd44896b62c7b67133b4d5a6a00266162"
|
||||
integrity sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==
|
||||
dependencies:
|
||||
"@ant-design/fast-color" "^3.0.0"
|
||||
|
||||
"@ant-design/cssinjs-utils@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz"
|
||||
@@ -194,12 +201,17 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.24.7"
|
||||
|
||||
"@ant-design/fast-color@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/fast-color/-/fast-color-3.0.0.tgz#fb5178203de825f284809538f5142203d0ef3d80"
|
||||
integrity sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==
|
||||
|
||||
"@ant-design/icons-svg@^4.4.0":
|
||||
version "4.4.2"
|
||||
resolved "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
|
||||
integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
|
||||
|
||||
"@ant-design/icons@^5.5.2", "@ant-design/icons@^5.6.1":
|
||||
"@ant-design/icons@^5.6.1":
|
||||
version "5.6.1"
|
||||
resolved "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz"
|
||||
integrity sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==
|
||||
@@ -210,6 +222,16 @@
|
||||
classnames "^2.2.6"
|
||||
rc-util "^5.31.1"
|
||||
|
||||
"@ant-design/icons@^6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.0.0.tgz#302c935b8b0b429e4444cbc45809247276186d94"
|
||||
integrity sha512-o0aCCAlHc1o4CQcapAwWzHeaW2x9F49g7P3IDtvtNXgHowtRWYb7kiubt8sQPFvfVIVU/jLw2hzeSlNt0FU+Uw==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^8.0.0"
|
||||
"@ant-design/icons-svg" "^4.4.0"
|
||||
"@rc-component/util" "^1.2.1"
|
||||
classnames "^2.2.6"
|
||||
|
||||
"@ant-design/react-slick@~1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz"
|
||||
@@ -2447,6 +2469,13 @@
|
||||
rc-resize-observer "^1.3.1"
|
||||
rc-util "^5.44.0"
|
||||
|
||||
"@rc-component/util@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.2.1.tgz#2c3158f11a4193478cec44ca42915da31f67d8a0"
|
||||
integrity sha512-AUVu6jO+lWjQnUOOECwu8iR0EdElQgWW5NBv5vP/Uf9dWbAX3udhMutRlkVXjuac2E40ghkFy+ve00mc/3Fymg==
|
||||
dependencies:
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@saucelabs/theme-github-codeblock@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/@saucelabs/theme-github-codeblock/-/theme-github-codeblock-0.3.0.tgz"
|
||||
|
||||
@@ -40,12 +40,13 @@ 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",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <3.0.0",
|
||||
"flask-appbuilder>=4.7.0, <5.0.0",
|
||||
"flask-appbuilder>=4.8.0, <5.0.0",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.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]
|
||||
|
||||
@@ -112,7 +112,7 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.7.0
|
||||
flask-appbuilder==4.8.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-babel==2.0.0
|
||||
# via flask-appbuilder
|
||||
|
||||
@@ -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
|
||||
@@ -151,52 +195,61 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.7.0
|
||||
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,7 +309,7 @@ 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
|
||||
@@ -266,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
|
||||
@@ -297,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
|
||||
@@ -322,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
|
||||
@@ -394,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
|
||||
@@ -425,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
|
||||
@@ -437,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
|
||||
@@ -456,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
|
||||
@@ -494,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
|
||||
@@ -516,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
|
||||
@@ -551,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
|
||||
@@ -566,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
|
||||
@@ -611,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
|
||||
@@ -639,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
|
||||
@@ -674,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
|
||||
@@ -703,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
|
||||
@@ -720,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
|
||||
@@ -776,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,430 +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.7.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
|
||||
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,6 +46,7 @@ export type UiConfigType = {
|
||||
urlParams?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
showRowLimitWarning?: boolean;
|
||||
};
|
||||
|
||||
export type EmbedDashboardParams = {
|
||||
@@ -133,6 +134,9 @@ export async function embedDashboard({
|
||||
if (dashboardUiConfig.emitDataMasks) {
|
||||
configNumber += 16;
|
||||
}
|
||||
if (dashboardUiConfig.showRowLimitWarning) {
|
||||
configNumber += 32;
|
||||
}
|
||||
}
|
||||
return configNumber;
|
||||
}
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
@@ -27,13 +27,6 @@ describe('Login view', () => {
|
||||
cy.visit(LOGIN);
|
||||
});
|
||||
|
||||
it('should load login page', () => {
|
||||
cy.getBySel('login-form').should('be.visible');
|
||||
cy.getBySel('username-input').should('be.visible');
|
||||
cy.getBySel('password-input').should('be.visible');
|
||||
cy.getBySel('login-button').should('be.visible');
|
||||
});
|
||||
|
||||
it('should redirect to login with incorrect username and password', () => {
|
||||
interceptLogin();
|
||||
cy.getBySel('login-form').should('be.visible');
|
||||
|
||||
@@ -174,46 +174,139 @@ describe('Native filters', () => {
|
||||
validateFilterContentOnDashboard(testItems.topTenChart.filterColumnYear);
|
||||
});
|
||||
|
||||
it('User can create a numerical range filter', () => {
|
||||
visitDashboard();
|
||||
enterNativeFilterEditModal(false);
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.numerical,
|
||||
testItems.filterNumericalColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
testItems.filterNumericalColumn,
|
||||
);
|
||||
saveNativeFilterSettings([]);
|
||||
|
||||
// Assertions
|
||||
cy.get('[data-test="range-filter-from-input"]')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
cy.get('[data-test="range-filter-from-input"]').type('{selectall}40');
|
||||
|
||||
cy.get('[data-test="range-filter-to-input"]')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
cy.get('[data-test="range-filter-to-input"]').type('{selectall}50');
|
||||
cy.get(nativeFilters.applyFilter).click({
|
||||
force: true,
|
||||
describe.only('Numerical Range Filter - Display Modes', () => {
|
||||
beforeEach(() => {
|
||||
visitDashboard();
|
||||
});
|
||||
|
||||
// Assert that the URL contains 'native_filters'
|
||||
cy.url().then(u => {
|
||||
const ur = new URL(u);
|
||||
expect(ur.search).to.include('native_filters');
|
||||
const expandFilterConfiguration = () => {
|
||||
cy.get('.ant-collapse-header')
|
||||
.contains('Filter Configuration')
|
||||
.should('be.visible')
|
||||
.then($header => {
|
||||
cy.wrap($header)
|
||||
.closest('.ant-collapse-item')
|
||||
.invoke('hasClass', 'ant-collapse-item-active')
|
||||
.then(isExpanded => {
|
||||
if (!isExpanded) cy.wrap($header).click();
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('.ant-collapse-content-box').should('be.visible');
|
||||
};
|
||||
|
||||
const selectRangeTypeOption = (label: string) => {
|
||||
cy.contains('Range Type')
|
||||
.should('be.visible')
|
||||
.closest('.ant-form-item')
|
||||
.within(() => {
|
||||
cy.get('.ant-select-selector').click();
|
||||
});
|
||||
|
||||
cy.get('.ant-select-dropdown:visible')
|
||||
.contains('.ant-select-item-option', label)
|
||||
.click();
|
||||
};
|
||||
|
||||
const applyAndAssertInputs = (from: string, to: string) => {
|
||||
// Set 'from' input
|
||||
cy.get('[data-test="range-filter-from-input"]').clear();
|
||||
cy.get('[data-test="range-filter-from-input"]').type(from);
|
||||
cy.get('[data-test="range-filter-from-input"]').blur();
|
||||
|
||||
// Set 'to' input
|
||||
cy.get('[data-test="range-filter-to-input"]').clear();
|
||||
cy.get('[data-test="range-filter-to-input"]').type(to);
|
||||
cy.get('[data-test="range-filter-to-input"]').blur();
|
||||
|
||||
// Assert values without chaining after .invoke()
|
||||
cy.get('[data-test="range-filter-from-input"]')
|
||||
.invoke('val')
|
||||
.should('equal', '40');
|
||||
.then(val => {
|
||||
expect(val).to.equal(from);
|
||||
});
|
||||
|
||||
// Assert that the "To" input has the correct value
|
||||
cy.get('[data-test="range-filter-to-input"]')
|
||||
.invoke('val')
|
||||
.should('equal', '50');
|
||||
.then(val => {
|
||||
expect(val).to.equal(to);
|
||||
});
|
||||
};
|
||||
|
||||
it('User can create a numerical range filter with "Range Inputs" display mode', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.numerical,
|
||||
testItems.filterNumericalColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
testItems.filterNumericalColumn,
|
||||
);
|
||||
|
||||
expandFilterConfiguration();
|
||||
selectRangeTypeOption('Range Inputs');
|
||||
|
||||
saveNativeFilterSettings([]);
|
||||
cy.wait(500); // allow filter to mount
|
||||
|
||||
applyAndAssertInputs('40', '70');
|
||||
});
|
||||
|
||||
it('User can change the display mode to "Slider"', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.numerical,
|
||||
testItems.filterNumericalColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
testItems.filterNumericalColumn,
|
||||
);
|
||||
|
||||
expandFilterConfiguration();
|
||||
|
||||
cy.contains('Range Type')
|
||||
.should('be.visible')
|
||||
.closest('.ant-form-item')
|
||||
.within(() => {
|
||||
cy.get('.ant-select-selector').click({ force: true });
|
||||
});
|
||||
|
||||
cy.get('.ant-select-dropdown:visible .ant-select-item-option')
|
||||
.contains(/^Slider$/)
|
||||
.click({ force: true });
|
||||
|
||||
cy.get('.ant-select-selector').should('contain.text', 'Slider');
|
||||
|
||||
saveNativeFilterSettings([]);
|
||||
|
||||
cy.get('.ant-slider', { timeout: 10000 }).should('be.visible');
|
||||
|
||||
cy.get('[data-test="range-filter-from-input"]', {
|
||||
timeout: 5000,
|
||||
}).should('not.exist');
|
||||
cy.get('[data-test="range-filter-to-input"]', { timeout: 5000 }).should(
|
||||
'not.exist',
|
||||
);
|
||||
});
|
||||
|
||||
it('User can change the display mode to "Slider and range input"', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
|
||||
// Re-create filter
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.numerical,
|
||||
testItems.filterNumericalColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
testItems.filterNumericalColumn,
|
||||
);
|
||||
|
||||
expandFilterConfiguration();
|
||||
selectRangeTypeOption('Slider and range input');
|
||||
|
||||
saveNativeFilterSettings([]);
|
||||
cy.wait(500);
|
||||
|
||||
applyAndAssertInputs('40', '70');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -363,9 +363,26 @@ export function saveNativeFilterSettings(charts: ChartSpec[]) {
|
||||
cy.get(nativeFilters.modal.footer)
|
||||
.contains('Save')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
.click({ force: true });
|
||||
|
||||
// Wait for modal to either close or remain open
|
||||
cy.get('body').should($body => {
|
||||
const modalExists = $body.find(nativeFilters.modal.container).length > 0;
|
||||
if (modalExists) {
|
||||
cy.get(nativeFilters.modal.footer)
|
||||
.contains('Save')
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure modal is closed
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
charts.forEach(waitForChartLoad);
|
||||
|
||||
// Wait for all charts to load
|
||||
charts.forEach(chart => {
|
||||
waitForChartLoad(chart);
|
||||
});
|
||||
}
|
||||
|
||||
/** ************************************************************************
|
||||
|
||||
@@ -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: {
|
||||
|
||||
13758
superset-frontend/package-lock.json
generated
13758
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -109,6 +109,7 @@
|
||||
"@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
|
||||
"@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
|
||||
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
|
||||
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
|
||||
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
||||
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
||||
"@types/d3-format": "^3.0.1",
|
||||
@@ -175,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",
|
||||
@@ -185,7 +186,6 @@
|
||||
"react-search-input": "^0.11.3",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-split": "^2.0.9",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
"react-table": "^7.8.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtualized-auto-sizer": "^1.0.25",
|
||||
@@ -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",
|
||||
@@ -262,7 +258,6 @@
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-resizable": "^3.0.8",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-ultimate-pagination": "^1.2.4",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.4",
|
||||
@@ -281,7 +276,7 @@
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"babel-plugin-typescript-to-proptypes": "^2.0.0",
|
||||
"cheerio": "1.0.0-rc.10",
|
||||
"cheerio": "1.1.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
@@ -339,13 +334,13 @@
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "5.4.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.98.0",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-manifest-plugin": "^5.0.0",
|
||||
"webpack-sources": "^3.2.3",
|
||||
"webpack-visualizer-plugin2": "^1.1.0"
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.1",
|
||||
"webpack-manifest-plugin": "^5.0.1",
|
||||
"webpack-sources": "^3.3.3",
|
||||
"webpack-visualizer-plugin2": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ace-builds": "^1.41.0",
|
||||
|
||||
@@ -28,7 +28,7 @@ import { <%= packageLabel %>Props, <%= packageLabel %>StylesProps } from './type
|
||||
// https://github.com/apache-superset/superset-ui/blob/master/packages/superset-ui-core/src/theme/index.ts
|
||||
|
||||
const Styles = styled.div<<%= packageLabel %>StylesProps>`
|
||||
background-color: ${({ theme }) => theme.colors.primary.light2};
|
||||
background-color: ${({ theme }) => theme.colorPrimaryBg};
|
||||
padding: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
height: ${({ height }) => height}px;
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
"dependencies": {
|
||||
"chalk": "^5.4.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"yeoman-generator": "^7.4.0",
|
||||
"yeoman-generator": "^7.5.1",
|
||||
"yosay": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"jest": "^30.0.2",
|
||||
"yeoman-test": "^8.3.0"
|
||||
"yeoman-test": "^10.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 4.0.0",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"@fontsource/fira-code": "^5.0.18",
|
||||
"@fontsource/inter": "^5.0.20",
|
||||
"@fontsource/fira-code": "^5.2.6",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"ace-builds": "^1.43.1",
|
||||
"brace": "^0.11.1",
|
||||
@@ -51,13 +51,14 @@
|
||||
"react-js-cron": "^5.2.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"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",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"reselect": "^4.0.0",
|
||||
"rison": "^0.1.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
@@ -72,8 +73,9 @@
|
||||
"@types/d3-time": "^3.0.4",
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/jquery": "^3.5.8",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
"@types/node": "^22.10.3",
|
||||
"@types/prop-types": "^15.7.2",
|
||||
|
||||
@@ -55,7 +55,11 @@ export enum AppSection {
|
||||
Embedded = 'EMBEDDED',
|
||||
}
|
||||
|
||||
export type FilterState = { value?: any; [key: string]: any };
|
||||
export type FilterState = {
|
||||
value?: any;
|
||||
customColumnLabel?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type DataMask = {
|
||||
extraFormData?: ExtraFormData;
|
||||
|
||||
@@ -55,6 +55,7 @@ export enum VizType {
|
||||
Step = 'echarts_timeseries_step',
|
||||
Sunburst = 'sunburst_v2',
|
||||
Table = 'table',
|
||||
TableAgGrid = 'ag-grid-table',
|
||||
TimePivot = 'time_pivot',
|
||||
TimeTable = 'time_table',
|
||||
Timeseries = 'echarts_timeseries',
|
||||
|
||||
@@ -165,7 +165,7 @@ export default {
|
||||
defaultValue: { summary: 'false' },
|
||||
},
|
||||
},
|
||||
dropdownRender: {
|
||||
popupRender: {
|
||||
control: false,
|
||||
description:
|
||||
'Custom render function for dropdown content. `(menus: ReactNode) => ReactNode`',
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* 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 { Typography, Flex, Space } from '@superset-ui/core/components';
|
||||
import CodeSyntaxHighlighter from '.';
|
||||
import type { CodeSyntaxHighlighterProps, SupportedLanguage } from '.';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
const languages: SupportedLanguage[] = ['sql', 'json', 'htmlbars', 'markdown'];
|
||||
|
||||
// Sample code for each language
|
||||
const sampleCode = {
|
||||
sql: `-- Complex SQL Query Example
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.email,
|
||||
COUNT(o.id) as total_orders,
|
||||
SUM(o.amount) as total_spent,
|
||||
AVG(o.amount) as avg_order_value
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.created_at >= '2023-01-01'
|
||||
AND u.status = 'active'
|
||||
GROUP BY u.id, u.username, u.email
|
||||
HAVING COUNT(o.id) > 0
|
||||
ORDER BY total_spent DESC, total_orders DESC
|
||||
LIMIT 50;`,
|
||||
|
||||
json: `{
|
||||
"user": {
|
||||
"id": 12345,
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"profile": {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"age": 30,
|
||||
"preferences": {
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"notifications": true
|
||||
}
|
||||
},
|
||||
"orders": [
|
||||
{
|
||||
"id": "order_001",
|
||||
"amount": 99.99,
|
||||
"status": "completed",
|
||||
"items": ["laptop", "mouse"]
|
||||
},
|
||||
{
|
||||
"id": "order_002",
|
||||
"amount": 49.99,
|
||||
"status": "pending",
|
||||
"items": ["keyboard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
|
||||
htmlbars: `{{!-- Handlebars Template Example --}}
|
||||
<div class="user-profile">
|
||||
<h1>Welcome, {{user.firstName}} {{user.lastName}}!</h1>
|
||||
|
||||
{{#if user.orders}}
|
||||
<div class="orders-section">
|
||||
<h2>Your Orders ({{user.orders.length}})</h2>
|
||||
|
||||
{{#each user.orders}}
|
||||
<div class="order-card {{status}}">
|
||||
<h3>Order #{{id}}</h3>
|
||||
<p class="amount">\${{amount}}</p>
|
||||
<p class="status">Status: {{capitalize status}}</p>
|
||||
|
||||
{{#if items}}
|
||||
<ul class="items">
|
||||
{{#each items}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="no-orders">No orders found.</p>
|
||||
{{/if}}
|
||||
</div>`,
|
||||
|
||||
markdown: `# CodeSyntaxHighlighter Component
|
||||
|
||||
A **themed syntax highlighter** for Superset that supports multiple languages and automatic theme switching.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎨 **Automatic theming** - Adapts to light/dark modes
|
||||
- ⚡ **Lazy loading** - Languages load on-demand for better performance
|
||||
- 🔧 **TypeScript support** - Full type safety
|
||||
- 📱 **Responsive** - Works on all screen sizes
|
||||
|
||||
## Supported Languages
|
||||
|
||||
| Language | Extension | Use Case |
|
||||
|----------|-----------|----------|
|
||||
| SQL | \`.sql\` | Database queries |
|
||||
| JSON | \`.json\` | Data interchange |
|
||||
| HTML/Handlebars | \`.hbs\` | Templates |
|
||||
| Markdown | \`.md\` | Documentation |
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`typescript
|
||||
import CodeSyntaxHighlighter from '@superset-ui/core/components/CodeSyntaxHighlighter';
|
||||
|
||||
<CodeSyntaxHighlighter language="sql">
|
||||
SELECT * FROM users WHERE active = true;
|
||||
</CodeSyntaxHighlighter>
|
||||
\`\`\`
|
||||
|
||||
> **Note**: Languages are loaded lazily for optimal performance!`,
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/CodeSyntaxHighlighter',
|
||||
component: CodeSyntaxHighlighter,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A themed syntax highlighter component that automatically adapts to Superset's light/dark themes and supports lazy loading of languages.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Gallery showing all supported languages
|
||||
export const LanguageGallery = () => (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{languages.map(language => (
|
||||
<div key={language}>
|
||||
<Title
|
||||
level={3}
|
||||
style={{ textTransform: 'capitalize', marginBottom: 16 }}
|
||||
>
|
||||
{language.toUpperCase()} Example
|
||||
</Title>
|
||||
<CodeSyntaxHighlighter language={language}>
|
||||
{sampleCode[language]}
|
||||
</CodeSyntaxHighlighter>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
|
||||
// Interactive playground
|
||||
export const InteractivePlayground = (args: CodeSyntaxHighlighterProps) => (
|
||||
<CodeSyntaxHighlighter {...args}>
|
||||
{args.children || sampleCode[args.language || 'sql']}
|
||||
</CodeSyntaxHighlighter>
|
||||
);
|
||||
|
||||
InteractivePlayground.args = {
|
||||
language: 'sql',
|
||||
showLineNumbers: false,
|
||||
wrapLines: true,
|
||||
children: sampleCode.sql,
|
||||
};
|
||||
|
||||
InteractivePlayground.argTypes = {
|
||||
language: {
|
||||
control: { type: 'select' },
|
||||
options: languages,
|
||||
description: 'Programming language for syntax highlighting',
|
||||
},
|
||||
showLineNumbers: {
|
||||
control: { type: 'boolean' },
|
||||
description: 'Display line numbers alongside the code',
|
||||
},
|
||||
wrapLines: {
|
||||
control: { type: 'boolean' },
|
||||
description: 'Wrap long lines instead of showing horizontal scroll',
|
||||
},
|
||||
children: {
|
||||
control: { type: 'text' },
|
||||
description: 'Code content to highlight',
|
||||
},
|
||||
customStyle: {
|
||||
control: { type: 'object' },
|
||||
description: 'Custom CSS styles to apply to the syntax highlighter',
|
||||
},
|
||||
};
|
||||
|
||||
// Showcase different styling options
|
||||
export const StylingExamples = () => (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{/* Default styling */}
|
||||
<div>
|
||||
<Title level={3}>Default Styling</Title>
|
||||
<CodeSyntaxHighlighter language="sql">
|
||||
SELECT id, name FROM users WHERE active = true;
|
||||
</CodeSyntaxHighlighter>
|
||||
</div>
|
||||
|
||||
{/* With line numbers */}
|
||||
<div>
|
||||
<Title level={3}>With Line Numbers</Title>
|
||||
<CodeSyntaxHighlighter language="sql" showLineNumbers>
|
||||
{sampleCode.sql}
|
||||
</CodeSyntaxHighlighter>
|
||||
</div>
|
||||
|
||||
{/* Custom styling */}
|
||||
<div>
|
||||
<Title level={3}>Custom Styling (Compact)</Title>
|
||||
<CodeSyntaxHighlighter
|
||||
language="json"
|
||||
customStyle={{
|
||||
fontSize: '12px',
|
||||
padding: '12px',
|
||||
maxHeight: '200px',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{sampleCode.json}
|
||||
</CodeSyntaxHighlighter>
|
||||
</div>
|
||||
|
||||
{/* No line wrapping */}
|
||||
<div>
|
||||
<Title level={3}>No Line Wrapping</Title>
|
||||
<CodeSyntaxHighlighter
|
||||
language="sql"
|
||||
wrapLines={false}
|
||||
customStyle={{ maxWidth: '400px' }}
|
||||
>
|
||||
{`SELECT very_long_column_name, another_very_long_column_name, yet_another_extremely_long_column_name FROM very_long_table_name WHERE condition = 'this is a very long condition';`}
|
||||
</CodeSyntaxHighlighter>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
|
||||
// Performance and edge cases
|
||||
export const EdgeCases = () => (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{/* Very long single line */}
|
||||
<div>
|
||||
<Title level={3}>Very Long Single Line</Title>
|
||||
<CodeSyntaxHighlighter language="sql">
|
||||
{`SELECT ${'very_long_column_name, '.repeat(20)}id FROM users;`}
|
||||
</CodeSyntaxHighlighter>
|
||||
</div>
|
||||
|
||||
{/* Special characters */}
|
||||
<div>
|
||||
<Title level={3}>Special Characters & Escaping</Title>
|
||||
<CodeSyntaxHighlighter language="sql">
|
||||
{`SELECT * FROM "table-name" WHERE field = 'O\\'Brien' AND data = '{"key": "value"}';`}
|
||||
</CodeSyntaxHighlighter>
|
||||
</div>
|
||||
|
||||
{/* Multiple languages showcase */}
|
||||
<div>
|
||||
<Title level={3}>Quick Language Comparison</Title>
|
||||
<Flex gap="middle">
|
||||
<div style={{ flex: 1 }}>
|
||||
<Title level={4}>SQL</Title>
|
||||
<CodeSyntaxHighlighter language="sql">
|
||||
SELECT id, name FROM users;
|
||||
</CodeSyntaxHighlighter>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Title level={4}>JSON</Title>
|
||||
<CodeSyntaxHighlighter language="json">
|
||||
{`{"users": [{"id": 1, "name": "John"}]}`}
|
||||
</CodeSyntaxHighlighter>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
|
||||
// Theme testing helper
|
||||
export const ThemeShowcase = () => (
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Paragraph>
|
||||
<Text strong>Theme Testing:</Text> Switch between light and dark themes in
|
||||
Storybook to see automatic adaptation.
|
||||
</Paragraph>
|
||||
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{languages.map(language => (
|
||||
<div key={language}>
|
||||
<Title level={4} style={{ textTransform: 'uppercase' }}>
|
||||
{language}
|
||||
</Title>
|
||||
<CodeSyntaxHighlighter language={language}>
|
||||
{sampleCode[language].split('\n').slice(0, 5).join('\n')}
|
||||
</CodeSyntaxHighlighter>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 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 } from '../../spec';
|
||||
import CodeSyntaxHighlighter from './index';
|
||||
|
||||
// Simple mock that just returns the content
|
||||
jest.mock(
|
||||
'react-syntax-highlighter/dist/cjs/light',
|
||||
() =>
|
||||
function MockSyntaxHighlighter({ children, ...props }: any) {
|
||||
return (
|
||||
<pre data-testid="syntax-highlighter" data-language={props.language}>
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Mock the language modules
|
||||
jest.mock(
|
||||
'react-syntax-highlighter/dist/cjs/languages/hljs/sql',
|
||||
() => 'sql-mock',
|
||||
);
|
||||
jest.mock(
|
||||
'react-syntax-highlighter/dist/cjs/languages/hljs/json',
|
||||
() => 'json-mock',
|
||||
);
|
||||
jest.mock(
|
||||
'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars',
|
||||
() => 'html-mock',
|
||||
);
|
||||
jest.mock(
|
||||
'react-syntax-highlighter/dist/cjs/languages/hljs/markdown',
|
||||
() => 'md-mock',
|
||||
);
|
||||
|
||||
// Mock the styles
|
||||
jest.mock('react-syntax-highlighter/dist/cjs/styles/hljs/github', () => ({}));
|
||||
jest.mock(
|
||||
'react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark',
|
||||
() => ({}),
|
||||
);
|
||||
|
||||
describe('CodeSyntaxHighlighter', () => {
|
||||
it('renders code content', () => {
|
||||
render(<CodeSyntaxHighlighter>SELECT * FROM users;</CodeSyntaxHighlighter>);
|
||||
|
||||
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with default SQL language', () => {
|
||||
render(<CodeSyntaxHighlighter>SELECT * FROM users;</CodeSyntaxHighlighter>);
|
||||
|
||||
// Should show content (the important thing is content is visible)
|
||||
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with specified language', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="json">
|
||||
{`{ "key": "value" }`}
|
||||
</CodeSyntaxHighlighter>,
|
||||
);
|
||||
|
||||
// Should show content regardless of which element renders it
|
||||
expect(screen.getByText('{ "key": "value" }')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports all expected languages', () => {
|
||||
const languages = ['sql', 'json', 'htmlbars', 'markdown'] as const;
|
||||
|
||||
languages.forEach(language => {
|
||||
const { unmount } = render(
|
||||
<CodeSyntaxHighlighter language={language}>
|
||||
{`Test content for ${language}`}
|
||||
</CodeSyntaxHighlighter>,
|
||||
);
|
||||
|
||||
// Should render the content (either in fallback or syntax highlighter)
|
||||
expect(
|
||||
screen.getByText(`Test content for ${language}`),
|
||||
).toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders fallback pre element initially', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql">
|
||||
SELECT COUNT(*) FROM table;
|
||||
</CodeSyntaxHighlighter>,
|
||||
);
|
||||
|
||||
// Should render the content in some form
|
||||
expect(screen.getByText('SELECT COUNT(*) FROM table;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles special characters', () => {
|
||||
const specialContent = "SELECT * FROM `users` WHERE name = 'O\\'Brien';";
|
||||
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql">
|
||||
{specialContent}
|
||||
</CodeSyntaxHighlighter>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(specialContent)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts custom styles', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql" customStyle={{ fontSize: '16px' }}>
|
||||
SELECT * FROM users;
|
||||
</CodeSyntaxHighlighter>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts showLineNumbers prop', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql" showLineNumbers>
|
||||
SELECT * FROM users;
|
||||
</CodeSyntaxHighlighter>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts wrapLines prop', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql" wrapLines={false}>
|
||||
SELECT * FROM users;
|
||||
</CodeSyntaxHighlighter>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import SyntaxHighlighterBase from 'react-syntax-highlighter/dist/cjs/light';
|
||||
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
|
||||
import tomorrow from 'react-syntax-highlighter/dist/cjs/styles/hljs/tomorrow-night';
|
||||
import { themeObject } from '@superset-ui/core';
|
||||
|
||||
export type SupportedLanguage = 'sql' | 'htmlbars' | 'markdown' | 'json';
|
||||
|
||||
export interface CodeSyntaxHighlighterProps {
|
||||
children: string;
|
||||
language?: SupportedLanguage;
|
||||
customStyle?: React.CSSProperties;
|
||||
showLineNumbers?: boolean;
|
||||
wrapLines?: boolean;
|
||||
style?: any; // Override theme style if needed
|
||||
}
|
||||
|
||||
// Track which languages have been registered to avoid duplicate registrations
|
||||
const registeredLanguages = new Set<SupportedLanguage>();
|
||||
|
||||
// Language import functions - these will be called lazily
|
||||
const languageImporters = {
|
||||
sql: () => import('react-syntax-highlighter/dist/cjs/languages/hljs/sql'),
|
||||
htmlbars: () =>
|
||||
import('react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars'),
|
||||
markdown: () =>
|
||||
import('react-syntax-highlighter/dist/cjs/languages/hljs/markdown'),
|
||||
json: () => import('react-syntax-highlighter/dist/cjs/languages/hljs/json'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Lazily register a language for syntax highlighting
|
||||
*/
|
||||
const registerLanguage = async (language: SupportedLanguage): Promise<void> => {
|
||||
if (registeredLanguages.has(language)) {
|
||||
return; // Already registered
|
||||
}
|
||||
|
||||
try {
|
||||
const languageModule = await languageImporters[language]();
|
||||
SyntaxHighlighterBase.registerLanguage(language, languageModule.default);
|
||||
registeredLanguages.add(language);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load language ${language}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A themed syntax highlighter component that automatically adapts to Superset's current theme.
|
||||
* Supports light/dark mode switching and provides consistent styling across the application.
|
||||
* Languages are loaded lazily to improve initial page load performance.
|
||||
* Uses ultra-neutral themes for professional, consistent appearance.
|
||||
*/
|
||||
export const CodeSyntaxHighlighter: React.FC<CodeSyntaxHighlighterProps> = ({
|
||||
children,
|
||||
language = 'sql',
|
||||
customStyle = {},
|
||||
showLineNumbers = false,
|
||||
wrapLines = true,
|
||||
style: overrideStyle,
|
||||
}) => {
|
||||
const [isLanguageReady, setIsLanguageReady] = useState(
|
||||
registeredLanguages.has(language),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadLanguage = async () => {
|
||||
if (!registeredLanguages.has(language)) {
|
||||
await registerLanguage(language);
|
||||
setIsLanguageReady(true);
|
||||
}
|
||||
};
|
||||
|
||||
loadLanguage();
|
||||
}, [language]);
|
||||
|
||||
const isDark = themeObject.isThemeDark();
|
||||
const themeStyle = overrideStyle || (isDark ? tomorrow : github);
|
||||
|
||||
const defaultCustomStyle: React.CSSProperties = {
|
||||
background: themeObject.theme.colorBgElevated,
|
||||
padding: themeObject.theme.sizeUnit * 4,
|
||||
border: 0,
|
||||
borderRadius: themeObject.theme.borderRadius,
|
||||
...customStyle,
|
||||
};
|
||||
|
||||
// Show a simple pre-formatted text while language is loading
|
||||
if (!isLanguageReady) {
|
||||
return (
|
||||
<pre
|
||||
style={{
|
||||
...defaultCustomStyle,
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SyntaxHighlighterBase
|
||||
language={language}
|
||||
style={themeStyle}
|
||||
customStyle={defaultCustomStyle}
|
||||
showLineNumbers={showLineNumbers}
|
||||
wrapLines={wrapLines}
|
||||
>
|
||||
{children}
|
||||
</SyntaxHighlighterBase>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to preload specific languages if needed
|
||||
* This can be called strategically in components that know they'll need certain languages
|
||||
*/
|
||||
export const preloadLanguages = async (
|
||||
languages: SupportedLanguage[],
|
||||
): Promise<void> => {
|
||||
const promises = languages
|
||||
.filter(lang => !registeredLanguages.has(lang))
|
||||
.map(registerLanguage);
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
export default CodeSyntaxHighlighter;
|
||||
@@ -38,7 +38,7 @@ const StyledCollapse = styled((props: CollapseProps) => (
|
||||
|
||||
${({ expandIconPosition }) =>
|
||||
expandIconPosition &&
|
||||
expandIconPosition === 'right' &&
|
||||
expandIconPosition === 'end' &&
|
||||
`
|
||||
.anticon.anticon-right.ant-collapse-arrow > svg {
|
||||
transform: rotate(90deg) !important;
|
||||
@@ -72,7 +72,7 @@ const StyledCollapse = styled((props: CollapseProps) => (
|
||||
.ant-collapse-header {
|
||||
${({ expandIconPosition }) =>
|
||||
expandIconPosition &&
|
||||
expandIconPosition === 'right' &&
|
||||
expandIconPosition === 'end' &&
|
||||
`
|
||||
.anticon.anticon-right.ant-collapse-arrow > svg {
|
||||
transform: rotate(-90deg) !important;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export const MenuDotsDropdown = ({
|
||||
iconOrientation = IconOrientation.Vertical,
|
||||
...rest
|
||||
}: MenuDotsDropdownProps) => (
|
||||
<AntdDropdown dropdownRender={() => overlay} {...rest}>
|
||||
<AntdDropdown popupRender={() => overlay} {...rest}>
|
||||
<MenuDotsWrapper data-test="dropdown-trigger">
|
||||
{RenderIcon(iconOrientation)}
|
||||
</MenuDotsWrapper>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Tooltip } from '../Tooltip';
|
||||
import type { DropdownButtonProps } from './types';
|
||||
|
||||
export const DropdownButton = ({
|
||||
dropdownRender,
|
||||
popupRender,
|
||||
tooltip,
|
||||
tooltipPlacement,
|
||||
children,
|
||||
@@ -51,7 +51,7 @@ export const DropdownButton = ({
|
||||
`;
|
||||
const button = (
|
||||
<Dropdown.Button
|
||||
dropdownRender={dropdownRender}
|
||||
popupRender={popupRender}
|
||||
{...rest}
|
||||
css={[
|
||||
defaultBtnCss,
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
CSSProperties,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
ReactElement,
|
||||
RefObject,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
@@ -26,17 +28,85 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { Global } from '@emotion/react';
|
||||
import { css, t, useTheme, usePrevious } from '@superset-ui/core';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import { Badge, Icons, Button, Tooltip, Popover } from '..';
|
||||
import type {
|
||||
DropdownContainerProps,
|
||||
DropdownItem,
|
||||
DropdownRef,
|
||||
} from './types';
|
||||
/**
|
||||
* Container item.
|
||||
*/
|
||||
export interface DropdownItem {
|
||||
/**
|
||||
* String that uniquely identifies the item.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The element to be rendered.
|
||||
*/
|
||||
element: ReactElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontal container that displays overflowed items in a dropdown.
|
||||
* It shows an indicator of how many items are currently overflowing.
|
||||
*/
|
||||
export interface DropdownContainerProps {
|
||||
/**
|
||||
* Array of items. The id property is used to uniquely identify
|
||||
* the elements when rendering or dealing with event handlers.
|
||||
*/
|
||||
items: DropdownItem[];
|
||||
/**
|
||||
* Event handler called every time an element moves between
|
||||
* main container and dropdown.
|
||||
*/
|
||||
onOverflowingStateChange?: (overflowingState: {
|
||||
notOverflowed: string[];
|
||||
overflowed: string[];
|
||||
}) => void;
|
||||
/**
|
||||
* Option to customize the content of the dropdown.
|
||||
*/
|
||||
dropdownContent?: (overflowedItems: DropdownItem[]) => ReactElement;
|
||||
/**
|
||||
* Dropdown ref.
|
||||
*/
|
||||
dropdownRef?: RefObject<HTMLDivElement>;
|
||||
/**
|
||||
* Dropdown additional style properties.
|
||||
*/
|
||||
dropdownStyle?: CSSProperties;
|
||||
/**
|
||||
* Displayed count in the dropdown trigger.
|
||||
*/
|
||||
dropdownTriggerCount?: number;
|
||||
/**
|
||||
* Icon of the dropdown trigger.
|
||||
*/
|
||||
dropdownTriggerIcon?: ReactElement;
|
||||
/**
|
||||
* Text of the dropdown trigger.
|
||||
*/
|
||||
dropdownTriggerText?: string;
|
||||
/**
|
||||
* Text of the dropdown trigger tooltip
|
||||
*/
|
||||
dropdownTriggerTooltip?: ReactNode | null;
|
||||
/**
|
||||
* Main container additional style properties.
|
||||
*/
|
||||
style?: CSSProperties;
|
||||
/**
|
||||
* Force render popover content before it's first opened
|
||||
*/
|
||||
forceRender?: boolean;
|
||||
}
|
||||
|
||||
export type DropdownRef = HTMLDivElement & { open: () => void };
|
||||
|
||||
const MAX_HEIGHT = 500;
|
||||
|
||||
@@ -73,6 +143,37 @@ export const DropdownContainer = forwardRef(
|
||||
|
||||
const [showOverflow, setShowOverflow] = useState(false);
|
||||
|
||||
// callback to update item widths so that the useLayoutEffect runs whenever
|
||||
// width of any of the child changes
|
||||
const recalculateItemWidths = useCallback(() => {
|
||||
const mainItemsContainerNode = current?.children.item(0);
|
||||
if (mainItemsContainerNode) {
|
||||
const visibleChildrenElements = Array.from(
|
||||
mainItemsContainerNode.children,
|
||||
);
|
||||
setItemsWidth(prevGlobalWidths => {
|
||||
if (prevGlobalWidths.length !== items.length) {
|
||||
return prevGlobalWidths;
|
||||
}
|
||||
|
||||
const newGlobalWidths = [...prevGlobalWidths];
|
||||
let changed = false;
|
||||
visibleChildrenElements.forEach((child, indexInVisible) => {
|
||||
const originalItemIndex = indexInVisible;
|
||||
if (originalItemIndex < newGlobalWidths.length) {
|
||||
const newWidth = child.getBoundingClientRect().width;
|
||||
if (newGlobalWidths[originalItemIndex] !== newWidth) {
|
||||
newGlobalWidths[originalItemIndex] = newWidth;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return changed ? newGlobalWidths : prevGlobalWidths;
|
||||
});
|
||||
}
|
||||
}, [current?.children, items.length]);
|
||||
|
||||
const reduceItems = (items: DropdownItem[]): [DropdownItem[], string[]] =>
|
||||
items.reduce(
|
||||
([items, ids], item) => {
|
||||
@@ -122,24 +223,7 @@ export const DropdownContainer = forwardRef(
|
||||
childrenArray.map(child => resizeObserver.unobserve(child));
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [items.length]);
|
||||
|
||||
// callback to update item widths so that the useLayoutEffect runs whenever
|
||||
// width of any of the child changes
|
||||
const recalculateItemWidths = () => {
|
||||
const container = current?.children.item(0);
|
||||
if (container) {
|
||||
const { children } = container;
|
||||
const childrenArray = Array.from(children);
|
||||
|
||||
const currentWidths = childrenArray.map(
|
||||
child => child.getBoundingClientRect().width,
|
||||
);
|
||||
|
||||
// Update state with new widths
|
||||
setItemsWidth(currentWidths);
|
||||
}
|
||||
};
|
||||
}, [items.length, current, recalculateItemWidths]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (popoverVisible) {
|
||||
@@ -206,6 +290,7 @@ export const DropdownContainer = forwardRef(
|
||||
overflowedItems.length,
|
||||
previousWidth,
|
||||
width,
|
||||
popoverVisible,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -376,5 +461,3 @@ export const DropdownContainer = forwardRef(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export type { DropdownItem, DropdownRef };
|
||||
|
||||
@@ -64,7 +64,7 @@ export const FaveStar = ({
|
||||
<Icons.StarFilled
|
||||
aria-label="starred"
|
||||
iconSize="l"
|
||||
iconColor={theme.colors.warning.base}
|
||||
iconColor={theme.colorWarning}
|
||||
name="favorite-selected"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -25,7 +25,7 @@ import { BaseIconComponent } from './BaseIcon';
|
||||
const AsyncIcon = (props: IconType) => {
|
||||
const [, setLoaded] = useState(false);
|
||||
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
|
||||
const { fileName } = props;
|
||||
const { fileName, ...restProps } = props;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -46,7 +46,7 @@ const AsyncIcon = (props: IconType) => {
|
||||
return (
|
||||
<BaseIconComponent
|
||||
component={ImportedSVG.current || TransparentIcon}
|
||||
{...props}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,11 +43,12 @@ export const BaseIconComponent: React.FC<
|
||||
iconSize,
|
||||
viewBox,
|
||||
customIcons,
|
||||
fileName,
|
||||
...rest
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const whatRole = rest?.onClick ? 'button' : 'img';
|
||||
const ariaLabel = genAriaLabel(rest.fileName || '');
|
||||
const ariaLabel = genAriaLabel(fileName || '');
|
||||
const style = {
|
||||
color: iconColor,
|
||||
fontSize: iconSize
|
||||
|
||||
@@ -37,7 +37,7 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
|
||||
datasetType === 'physical' ? (
|
||||
<Icons.InsertRowAboveOutlined
|
||||
iconSize={SIZE}
|
||||
iconColor={theme.colors.primary.dark1}
|
||||
iconColor={theme.colorPrimary}
|
||||
/>
|
||||
) : (
|
||||
<Icons.ConsoleSqlOutlined iconSize={SIZE} />
|
||||
|
||||
@@ -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)`};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -214,13 +223,14 @@ const CustomModal = ({
|
||||
resizable = false,
|
||||
resizableConfig = defaultResizableConfig(hideFooter),
|
||||
draggableConfig,
|
||||
destroyOnClose,
|
||||
destroyOnHidden,
|
||||
openerRef,
|
||||
...rest
|
||||
}: ModalProps) => {
|
||||
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}
|
||||
@@ -344,7 +358,7 @@ const CustomModal = ({
|
||||
mask={shouldShowMask}
|
||||
draggable={draggable}
|
||||
resizable={resizable}
|
||||
destroyOnClose={destroyOnClose}
|
||||
destroyOnHidden={destroyOnHidden}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface ModalProps {
|
||||
show: boolean;
|
||||
name?: string;
|
||||
title: ReactNode;
|
||||
width?: string;
|
||||
width?: string | number;
|
||||
maxWidth?: string;
|
||||
responsive?: boolean;
|
||||
hideFooter?: boolean;
|
||||
@@ -47,7 +47,7 @@ export interface ModalProps {
|
||||
resizableConfig?: ResizableProps;
|
||||
draggable?: boolean;
|
||||
draggableConfig?: DraggableProps;
|
||||
destroyOnClose?: boolean;
|
||||
destroyOnHidden?: boolean;
|
||||
maskClosable?: boolean;
|
||||
zIndex?: number;
|
||||
bodyStyle?: CSSProperties;
|
||||
|
||||
@@ -39,7 +39,7 @@ export interface ModalTriggerProps {
|
||||
resizableConfig?: any;
|
||||
draggable?: boolean;
|
||||
draggableConfig?: any;
|
||||
destroyOnClose?: boolean;
|
||||
destroyOnHidden?: boolean;
|
||||
}
|
||||
|
||||
export interface ModalTriggerRef {
|
||||
@@ -63,7 +63,7 @@ export const ModalTrigger = forwardRef(
|
||||
tooltip,
|
||||
modalFooter,
|
||||
triggerNode,
|
||||
destroyOnClose = true,
|
||||
destroyOnHidden = true,
|
||||
modalBody,
|
||||
draggableConfig = {},
|
||||
resizableConfig = {},
|
||||
@@ -120,7 +120,7 @@ export const ModalTrigger = forwardRef(
|
||||
resizableConfig={resizableConfig}
|
||||
draggable={draggable}
|
||||
draggableConfig={draggableConfig}
|
||||
destroyOnClose={destroyOnClose}
|
||||
destroyOnHidden={destroyOnHidden}
|
||||
>
|
||||
{modalBody}
|
||||
</Modal>
|
||||
|
||||
@@ -34,7 +34,7 @@ export const menuTriggerStyles = (theme: SupersetTheme) => css`
|
||||
width: ${theme.sizeUnit * 8}px;
|
||||
height: ${theme.sizeUnit * 8}px;
|
||||
padding: 0;
|
||||
border: 1px solid ${theme.colors.primary.dark2};
|
||||
border: 1px solid ${theme.colorPrimary};
|
||||
|
||||
&.ant-btn > span.anticon {
|
||||
line-height: 0;
|
||||
@@ -151,7 +151,7 @@ export const PageHeaderWithActions = ({
|
||||
{showMenuDropdown && (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
dropdownRender={() => additionalActionsMenu}
|
||||
popupRender={() => additionalActionsMenu}
|
||||
{...menuDropdownProps}
|
||||
>
|
||||
<Button
|
||||
@@ -163,7 +163,7 @@ export const PageHeaderWithActions = ({
|
||||
data-test="actions-trigger"
|
||||
>
|
||||
<Icons.EllipsisOutlined
|
||||
iconColor={theme.colors.primary.dark2}
|
||||
iconColor={theme.colorPrimary}
|
||||
iconSize="l"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -129,7 +129,7 @@ const AsyncSelect = forwardRef(
|
||||
onError,
|
||||
onChange,
|
||||
onClear,
|
||||
onDropdownVisibleChange,
|
||||
onOpenChange,
|
||||
onDeselect,
|
||||
onSearch,
|
||||
onSelect,
|
||||
@@ -441,12 +441,12 @@ const AsyncSelect = forwardRef(
|
||||
}
|
||||
}
|
||||
|
||||
if (onDropdownVisibleChange) {
|
||||
onDropdownVisibleChange(isDropdownVisible);
|
||||
if (onOpenChange) {
|
||||
onOpenChange(isDropdownVisible);
|
||||
}
|
||||
};
|
||||
|
||||
const dropdownRender = (
|
||||
const popupRender = (
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
) =>
|
||||
dropDownRenderHelper(
|
||||
@@ -605,7 +605,7 @@ const AsyncSelect = forwardRef(
|
||||
}
|
||||
data-test={ariaLabel || name}
|
||||
autoClearSearchValue={autoClearSearchValue}
|
||||
dropdownRender={dropdownRender}
|
||||
popupRender={popupRender}
|
||||
filterOption={handleFilterOption}
|
||||
filterSort={sortComparatorWithSearch}
|
||||
getPopupContainer={
|
||||
@@ -618,7 +618,7 @@ const AsyncSelect = forwardRef(
|
||||
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
|
||||
onBlur={handleOnBlur}
|
||||
onDeselect={handleOnDeselect}
|
||||
onDropdownVisibleChange={handleOnDropdownVisibleChange}
|
||||
onOpenChange={handleOnDropdownVisibleChange}
|
||||
// @ts-ignore
|
||||
onPaste={onPaste}
|
||||
onPopupScroll={handlePagination}
|
||||
|
||||
@@ -104,7 +104,7 @@ const Select = forwardRef(
|
||||
onBlur,
|
||||
onChange,
|
||||
onClear,
|
||||
onDropdownVisibleChange,
|
||||
onOpenChange,
|
||||
onDeselect,
|
||||
onSearch,
|
||||
onSelect,
|
||||
@@ -398,8 +398,8 @@ const Select = forwardRef(
|
||||
if (!isDropdownVisible) {
|
||||
setSelectOptions(initialOptionsSorted);
|
||||
}
|
||||
if (onDropdownVisibleChange) {
|
||||
onDropdownVisibleChange(isDropdownVisible);
|
||||
if (onOpenChange) {
|
||||
onOpenChange(isDropdownVisible);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -492,7 +492,7 @@ const Select = forwardRef(
|
||||
],
|
||||
);
|
||||
|
||||
const dropdownRender = (
|
||||
const popupRender = (
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
) =>
|
||||
dropDownRenderHelper(
|
||||
@@ -694,7 +694,7 @@ const Select = forwardRef(
|
||||
}
|
||||
data-test={ariaLabel || name}
|
||||
autoClearSearchValue={autoClearSearchValue}
|
||||
dropdownRender={dropdownRender}
|
||||
popupRender={popupRender}
|
||||
filterOption={handleFilterOption}
|
||||
filterSort={sortComparatorWithSearch}
|
||||
getPopupContainer={
|
||||
@@ -708,7 +708,7 @@ const Select = forwardRef(
|
||||
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
|
||||
onBlur={handleOnBlur}
|
||||
onDeselect={handleOnDeselect}
|
||||
onDropdownVisibleChange={handleOnDropdownVisibleChange}
|
||||
onOpenChange={handleOnDropdownVisibleChange}
|
||||
// @ts-ignore
|
||||
onPaste={onPaste}
|
||||
onPopupScroll={undefined}
|
||||
|
||||
@@ -45,7 +45,7 @@ export const StyledSelect = styled(Select, {
|
||||
})<{ headerPosition?: string; oneLine?: boolean }>`
|
||||
${({ theme, headerPosition, oneLine }) => `
|
||||
.ant-select-item-option-active:not(.ant-select-item-option-disabled) {
|
||||
outline: 2px solid ${theme.colors.primary.base};
|
||||
outline: 2px solid ${theme.colorPrimary};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
flex: ${headerPosition === 'left' ? 1 : 0};
|
||||
|
||||
@@ -62,7 +62,7 @@ export type AntdExposedProps = Pick<
|
||||
| 'onBlur'
|
||||
| 'onPopupScroll'
|
||||
| 'onSearch'
|
||||
| 'onDropdownVisibleChange'
|
||||
| 'onOpenChange'
|
||||
| 'optionRender'
|
||||
| 'placeholder'
|
||||
| 'showArrow'
|
||||
@@ -95,7 +95,7 @@ export interface BaseSelectProps extends AntdExposedProps {
|
||||
/**
|
||||
* Renders the dropdown
|
||||
*/
|
||||
dropdownRender?: (
|
||||
popupRender?: (
|
||||
menu: ReactElement<any, string | JSXElementConstructor<any>>,
|
||||
) => ReactElement<any, string | JSXElementConstructor<any>>;
|
||||
/**
|
||||
|
||||
@@ -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 />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -57,7 +57,7 @@ const StyledSaveBtn = styled(Button)`
|
||||
|
||||
const StyledWarningIcon = styled(Icons.WarningOutlined)`
|
||||
${({ theme }) => css`
|
||||
color: ${theme.colors.warning.base};
|
||||
color: ${theme.colorWarning};
|
||||
margin-right: ${theme.sizeUnit * 4}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -40,7 +40,7 @@ export const buildCustomFormatters = (
|
||||
const actualCurrencyFormat = currencyFormat?.symbol
|
||||
? currencyFormat
|
||||
: savedCurrencyFormats[metric];
|
||||
return actualCurrencyFormat
|
||||
return actualCurrencyFormat?.symbol
|
||||
? {
|
||||
...acc,
|
||||
[metric]: new CurrencyFormatter({
|
||||
|
||||
@@ -58,17 +58,6 @@ export const GlobalStyles = () => {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ant-dropdown,
|
||||
.ant-dropdown,
|
||||
.ant-select-dropdown,
|
||||
.ant-modal-wrap,
|
||||
.ant-modal-mask,
|
||||
.ant-picker-dropdown,
|
||||
.ant-popover,
|
||||
.ant-popover {
|
||||
z-index: ${theme.zIndexPopupBase} !important;
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,12 +22,20 @@ import React from 'react';
|
||||
import { theme as antdThemeImport, ConfigProvider } from 'antd';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
// @fontsource/* v5.1+ doesn't play nice with eslint-import plugin v2.31+
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/inter/200.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/inter/400.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/inter/500.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/inter/600.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/fira-code/400.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/fira-code/500.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/fira-code/600.css';
|
||||
|
||||
import {
|
||||
|
||||
@@ -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',
|
||||
@@ -60,6 +61,8 @@ export enum FeatureFlag {
|
||||
SlackEnableAvatars = 'SLACK_ENABLE_AVATARS',
|
||||
EnableDashboardScreenshotEndpoints = 'ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS',
|
||||
EnableDashboardDownloadWebDriverScreenshot = 'ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT',
|
||||
TableV2TimeComparisonEnabled = 'TABLE_V2_TIME_COMPARISON_ENABLED',
|
||||
AgGridTableEnabled = 'AG_GRID_TABLE_ENABLED',
|
||||
}
|
||||
|
||||
export type ScheduleQueriesProps = {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ export const TestComponent = ({
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
backgroundColor: theme.colors.primary.light5,
|
||||
color: theme.colors.grayscale.light3,
|
||||
backgroundColor: theme.colorPrimaryBg,
|
||||
color: theme.colorTextSecondary,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
@@ -66,7 +66,7 @@ export const TestComponent = ({
|
||||
{[width, height].join('x')}
|
||||
</div>
|
||||
<div className="formData" style={{ padding: 10 }}>
|
||||
<code style={{ color: theme.colors.primary.light2 }}>
|
||||
<code style={{ color: theme.colorTextSecondary }}>
|
||||
{JSON.stringify(formData)}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import {
|
||||
buildCustomFormatters,
|
||||
Currency,
|
||||
CurrencyFormatter,
|
||||
getCustomFormatter,
|
||||
getNumberFormatter,
|
||||
@@ -116,6 +117,19 @@ test('buildCustomFormatters uses dataset d3 format if not provided in control pa
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCustomFormatters returns NumberFormatter for a d3format with currency set to {}', () => {
|
||||
const customFormatters: Record<string, ValueFormatter> =
|
||||
buildCustomFormatters(
|
||||
['count'],
|
||||
{ count: {} as Currency },
|
||||
{ count: ',.2%' },
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(customFormatters.count).toBeInstanceOf(NumberFormatter);
|
||||
});
|
||||
|
||||
test('getCustomFormatter', () => {
|
||||
const customFormatters = {
|
||||
sum__num: new CurrencyFormatter({
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mihkeleidast/storybook-addon-source": "^1.0.1",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@storybook/addon-actions": "8.1.11",
|
||||
"@storybook/addon-actions": "9.0.8",
|
||||
"@storybook/addon-controls": "8.1.11",
|
||||
"@storybook/addon-links": "8.1.11",
|
||||
"@storybook/react": "8.1.11",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@storybook/react-webpack5": "8.2.9",
|
||||
"babel-loader": "^10.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2"
|
||||
"react": "^19.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
|
||||
@@ -24,21 +24,21 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@deck.gl/aggregation-layers": "^9.1.9",
|
||||
"@deck.gl/core": "^9.1.9",
|
||||
"@deck.gl/geo-layers": "^9.1.9",
|
||||
"@deck.gl/layers": "^9.1.9",
|
||||
"@deck.gl/react": "^9.1.9",
|
||||
"@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",
|
||||
"@types/geojson": "^7946.0.15",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"bootstrap-slider": "^11.0.2",
|
||||
"d3-array": "^1.2.4",
|
||||
"d3-color": "^1.4.1",
|
||||
"d3-scale": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"ngeohash": "^0.6.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"underscore": "^1.13.7",
|
||||
"urijs": "^1.19.11",
|
||||
@@ -46,6 +46,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mapbox__geojson-extent": "^1.0.3",
|
||||
"@types/ngeohash": "^0.6.8",
|
||||
"@types/underscore": "^1.13.0",
|
||||
"@types/urijs": "^1.19.25"
|
||||
},
|
||||
|
||||
@@ -28,24 +28,28 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
CategoricalColorNamespace,
|
||||
Datasource,
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
} from '@superset-ui/core';
|
||||
import type { Layer } from '@deck.gl/core';
|
||||
import Legend from './components/Legend';
|
||||
import { hexToRGB } from './utils/colors';
|
||||
import sandboxedEval from './utils/sandbox';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import fitViewport, { Viewport } from './utils/fitViewport';
|
||||
import {
|
||||
DeckGLContainerHandle,
|
||||
DeckGLContainerStyledWrapper,
|
||||
} from './DeckGLContainer';
|
||||
import { Point } from './types';
|
||||
import { getLayerType } from './factory';
|
||||
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;
|
||||
|
||||
@@ -54,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;
|
||||
}
|
||||
@@ -78,10 +88,14 @@ export type CategoricalDeckGLContainerProps = {
|
||||
height: number;
|
||||
width: number;
|
||||
viewport: Viewport;
|
||||
getLayer: getLayerType<unknown>;
|
||||
getLayer: GetLayerType<unknown>;
|
||||
payload: JsonObject;
|
||||
onAddFilter?: HandlerFunction;
|
||||
setControlValue: (control: string, value: JsonValue) => void;
|
||||
filterState: FilterState;
|
||||
setDataMask: SetDataMaskHook;
|
||||
onContextMenu: HandlerFunction;
|
||||
emitCrossFilters: boolean;
|
||||
};
|
||||
|
||||
const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
@@ -128,29 +142,91 @@ 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 { getLayer, payload, formData: fd, onAddFilter } = props;
|
||||
const {
|
||||
getLayer,
|
||||
payload,
|
||||
formData: fd,
|
||||
onAddFilter,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
} = 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) {
|
||||
@@ -169,13 +245,17 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
};
|
||||
|
||||
return [
|
||||
getLayer(
|
||||
fd,
|
||||
filteredPayload,
|
||||
getLayer({
|
||||
formData: fd,
|
||||
payload: filteredPayload,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
props.datasource,
|
||||
) as Layer,
|
||||
datasource: props.datasource,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
}) as Layer,
|
||||
];
|
||||
}, [addColor, categories, props, setTooltip]);
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ import {
|
||||
forwardRef,
|
||||
memo,
|
||||
ReactNode,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import { StaticMap } from 'react-map-gl';
|
||||
@@ -37,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
|
||||
|
||||
@@ -64,6 +60,14 @@ export const DeckGLContainer = memo(
|
||||
const [lastUpdate, setLastUpdate] = useState<number | null>(null);
|
||||
const [viewState, setViewState] = useState(props.viewport);
|
||||
const prevViewport = usePrevious(props.viewport);
|
||||
const glContextRef = useRef<WebGL2RenderingContext | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
glContextRef.current?.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({ setTooltip }), []);
|
||||
|
||||
@@ -98,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 =>
|
||||
@@ -120,13 +110,19 @@ export const DeckGLContainer = memo(
|
||||
}
|
||||
|
||||
return props.layers as Layer[];
|
||||
}, [props.layers, props.mapStyle]);
|
||||
}, [props.layers]);
|
||||
|
||||
const { children = null, height, width } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: 'relative', width, height }}>
|
||||
<div
|
||||
style={{ position: 'relative', width, height }}
|
||||
onContextMenu={(e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<DeckGL
|
||||
controller
|
||||
width={width}
|
||||
@@ -134,14 +130,15 @@ export const DeckGLContainer = memo(
|
||||
layers={layers()}
|
||||
viewState={viewState}
|
||||
onViewStateChange={onViewStateChange}
|
||||
onAfterRender={context => {
|
||||
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>
|
||||
|
||||
@@ -22,11 +22,15 @@
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import {
|
||||
AdhocFilter,
|
||||
Datasource,
|
||||
ensureIsArray,
|
||||
HandlerFunction,
|
||||
isDefined,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
QueryFormData,
|
||||
QueryObjectFilterClause,
|
||||
SupersetClient,
|
||||
usePrevious,
|
||||
} from '@superset-ui/core';
|
||||
@@ -107,70 +111,175 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getLayerIndex = useCallback(
|
||||
(sliceId: number, payloadIndex: number, deckSlices?: number[]): number =>
|
||||
deckSlices ? deckSlices.indexOf(sliceId) : payloadIndex,
|
||||
[],
|
||||
);
|
||||
|
||||
const processLayerFilters = useCallback(
|
||||
(
|
||||
subslice: JsonObject,
|
||||
formData: QueryFormData,
|
||||
layerIndex: number,
|
||||
): {
|
||||
extraFilters: (AdhocFilter | QueryObjectFilterClause)[];
|
||||
adhocFilters: AdhocFilter[];
|
||||
} => {
|
||||
const layerFilterScope = formData.layer_filter_scope;
|
||||
|
||||
const extraFilters: (AdhocFilter | QueryObjectFilterClause)[] = [
|
||||
...(subslice.form_data.extra_filters || []),
|
||||
...(formData.extra_filters || []),
|
||||
];
|
||||
|
||||
const adhocFilters: AdhocFilter[] = [
|
||||
...(subslice.form_data?.adhoc_filters || []),
|
||||
];
|
||||
|
||||
if (layerFilterScope) {
|
||||
const filterDataMapping = formData.filter_data_mapping || {};
|
||||
let shouldAddDashboardAdhocFilters = false;
|
||||
|
||||
Object.entries(layerFilterScope).forEach(
|
||||
([filterId, filterScope]: [string, number[]]) => {
|
||||
const shouldApplyFilter =
|
||||
ensureIsArray(filterScope).includes(layerIndex);
|
||||
|
||||
if (shouldApplyFilter) {
|
||||
shouldAddDashboardAdhocFilters = true;
|
||||
const filtersFromThisFilter = filterDataMapping[filterId] || [];
|
||||
extraFilters.push(...filtersFromThisFilter);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (shouldAddDashboardAdhocFilters) {
|
||||
const dashboardAdhocFilters = formData.adhoc_filters || [];
|
||||
adhocFilters.push(...dashboardAdhocFilters);
|
||||
}
|
||||
} else {
|
||||
const originalExtraFormDataFilters =
|
||||
formData.extra_form_data?.filters || [];
|
||||
extraFilters.push(...originalExtraFormDataFilters);
|
||||
|
||||
const dashboardAdhocFilters = formData.adhoc_filters || [];
|
||||
adhocFilters.push(...dashboardAdhocFilters);
|
||||
}
|
||||
|
||||
return { extraFilters, adhocFilters };
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const createLayerFromData = useCallback(
|
||||
(subslice: JsonObject, json: JsonObject): Layer =>
|
||||
// @ts-ignore TODO(hainenber): define proper type for `form_data.viz_type` and call signature for functions in layerGenerators.
|
||||
layerGenerators[subslice.form_data.viz_type](
|
||||
subslice.form_data,
|
||||
json,
|
||||
props.onAddFilter,
|
||||
setTooltip,
|
||||
props.datasource,
|
||||
[],
|
||||
props.onSelect,
|
||||
),
|
||||
[props.onAddFilter, props.onSelect, props.datasource, setTooltip],
|
||||
);
|
||||
|
||||
const loadSingleLayer = useCallback(
|
||||
(
|
||||
subslice: JsonObject,
|
||||
formData: QueryFormData,
|
||||
payloadIndex: number,
|
||||
): void => {
|
||||
const layerIndex = getLayerIndex(
|
||||
subslice.slice_id,
|
||||
payloadIndex,
|
||||
formData.deck_slices,
|
||||
);
|
||||
let extraFilters: (AdhocFilter | QueryObjectFilterClause)[] = [];
|
||||
let adhocFilters: AdhocFilter[] = [];
|
||||
const isExplore = (window.location.href || '').includes('explore');
|
||||
if (isExplore) {
|
||||
// in explore all the filters are in the adhoc_filters
|
||||
const adhocFiltersFromFormData = formData.adhoc_filters || [];
|
||||
const finalAdhocFilters = adhocFiltersFromFormData
|
||||
.map((filter: AdhocFilter & { layerFilterScope?: number[] }) => {
|
||||
if (!isDefined(filter?.layerFilterScope)) {
|
||||
return filter;
|
||||
}
|
||||
if (
|
||||
Array.isArray(filter.layerFilterScope) &&
|
||||
filter.layerFilterScope.length > 0
|
||||
) {
|
||||
if (filter.layerFilterScope.includes(-1)) {
|
||||
return filter;
|
||||
}
|
||||
if (filter.layerFilterScope.includes(layerIndex)) {
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(filter => isDefined(filter));
|
||||
adhocFilters = finalAdhocFilters as AdhocFilter[];
|
||||
} else {
|
||||
const {
|
||||
extraFilters: processLayerFiltersResultExtraFilters,
|
||||
adhocFilters: processLayerFiltersResultAdhocFilters,
|
||||
} = processLayerFilters(subslice, formData, layerIndex);
|
||||
extraFilters = processLayerFiltersResultExtraFilters;
|
||||
adhocFilters = processLayerFiltersResultAdhocFilters;
|
||||
}
|
||||
|
||||
const subsliceCopy = {
|
||||
...subslice,
|
||||
form_data: {
|
||||
...subslice.form_data,
|
||||
extra_filters: extraFilters,
|
||||
adhoc_filters: adhocFilters,
|
||||
},
|
||||
} as any as JsonObject & { slice_id: number };
|
||||
|
||||
const url = getExploreLongUrl(subsliceCopy.form_data, 'json');
|
||||
|
||||
if (url) {
|
||||
SupersetClient.get({ endpoint: url })
|
||||
.then(({ json }) => {
|
||||
const layer = createLayerFromData(subsliceCopy, json);
|
||||
setSubSlicesLayers(subSlicesLayers => ({
|
||||
...subSlicesLayers,
|
||||
[subsliceCopy.slice_id]: layer,
|
||||
}));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(
|
||||
`Error loading layer for slice ${subsliceCopy.slice_id}:`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
[getLayerIndex, processLayerFilters, createLayerFromData],
|
||||
);
|
||||
|
||||
const loadLayers = useCallback(
|
||||
(formData: QueryFormData, payload: JsonObject, viewport?: Viewport) => {
|
||||
(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
viewport?: Viewport,
|
||||
): void => {
|
||||
setViewport(getAdjustedViewport());
|
||||
setSubSlicesLayers({});
|
||||
|
||||
payload.data.slices.forEach(
|
||||
(subslice: { slice_id: number } & JsonObject) => {
|
||||
// Filters applied to multi_deck are passed down to underlying charts
|
||||
// note that dashboard contextual information (filter_immune_slices and such) aren't
|
||||
// taken into consideration here
|
||||
const extra_filters = [
|
||||
...(subslice.form_data.extra_filters || []),
|
||||
...(formData.extra_filters || []),
|
||||
...(formData.extra_form_data?.filters || []),
|
||||
];
|
||||
|
||||
const adhoc_filters = [
|
||||
...(formData.adhoc_filters || []),
|
||||
...(subslice.formData?.adhoc_filters || []),
|
||||
...(formData.extra_form_data?.adhoc_filters || []),
|
||||
];
|
||||
|
||||
const subsliceCopy = {
|
||||
...subslice,
|
||||
form_data: {
|
||||
...subslice.form_data,
|
||||
extra_filters,
|
||||
adhoc_filters,
|
||||
},
|
||||
};
|
||||
|
||||
const url = getExploreLongUrl(subsliceCopy.form_data, 'json');
|
||||
|
||||
if (url) {
|
||||
SupersetClient.get({
|
||||
endpoint: url,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
// @ts-ignore TODO(hainenber): define proper type for `form_data.viz_type` and call signature for functions in layerGenerators.
|
||||
const layer = layerGenerators[subsliceCopy.form_data.viz_type](
|
||||
subsliceCopy.form_data,
|
||||
json,
|
||||
props.onAddFilter,
|
||||
setTooltip,
|
||||
props.datasource,
|
||||
[],
|
||||
props.onSelect,
|
||||
);
|
||||
setSubSlicesLayers(subSlicesLayers => ({
|
||||
...subSlicesLayers,
|
||||
[subsliceCopy.slice_id]: layer,
|
||||
}));
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
(subslice: { slice_id: number } & JsonObject, payloadIndex: number) => {
|
||||
loadSingleLayer(subslice, formData, payloadIndex);
|
||||
},
|
||||
);
|
||||
},
|
||||
[
|
||||
props.datasource,
|
||||
props.onAddFilter,
|
||||
props.onSelect,
|
||||
setTooltip,
|
||||
getAdjustedViewport,
|
||||
],
|
||||
[getAdjustedViewport, loadSingleLayer],
|
||||
);
|
||||
|
||||
const prevDeckSlices = usePrevious(props.formData.deck_slices);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -25,6 +25,11 @@ import {
|
||||
JsonObject,
|
||||
HandlerFunction,
|
||||
usePrevious,
|
||||
SetDataMaskHook,
|
||||
DataMask,
|
||||
FilterState,
|
||||
JsonValue,
|
||||
ContextMenuFilters,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
@@ -35,38 +40,59 @@ 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 = {
|
||||
type DeckGLComponentProps = {
|
||||
datasource: Datasource;
|
||||
formData: QueryFormData;
|
||||
height: number;
|
||||
onAddFilter: HandlerFunction;
|
||||
onContextMenu: HandlerFunction;
|
||||
payload: JsonObject;
|
||||
setControlValue: () => void;
|
||||
viewport: Viewport;
|
||||
width: number;
|
||||
filterState: FilterState;
|
||||
setDataMask: SetDataMaskHook;
|
||||
emitCrossFilters: boolean;
|
||||
};
|
||||
export interface getLayerType<T> {
|
||||
(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: HandlerFunction | undefined,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
datasource?: Datasource,
|
||||
): T;
|
||||
|
||||
export interface GetLayerTypeParams {
|
||||
formData: QueryFormData;
|
||||
payload: JsonObject;
|
||||
onAddFilter?: HandlerFunction;
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void;
|
||||
setDataMask?: (dataMask: DataMask) => void;
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
datasource?: Datasource;
|
||||
filterState?: FilterState;
|
||||
selected?: JsonObject[];
|
||||
onSelect?: (value: JsonValue) => void;
|
||||
emitCrossFilters?: boolean;
|
||||
}
|
||||
interface getPointsType {
|
||||
|
||||
export interface GetLayerType<T> {
|
||||
(params: GetLayerTypeParams): T;
|
||||
}
|
||||
|
||||
interface GetPointsType {
|
||||
(data: JsonObject[]): Point[];
|
||||
}
|
||||
|
||||
export function createDeckGLComponent(
|
||||
getLayer: getLayerType<unknown>,
|
||||
getPoints: getPointsType,
|
||||
getLayer: GetLayerType<unknown>,
|
||||
getPoints: GetPointsType,
|
||||
) {
|
||||
// Higher order component
|
||||
return memo((props: deckGLComponentProps) => {
|
||||
return memo((props: DeckGLComponentProps) => {
|
||||
const containerRef = useRef<DeckGLContainerHandle>();
|
||||
const prevFormData = usePrevious(props.formData);
|
||||
const prevFilterState = usePrevious(props.filterState);
|
||||
const prevPayload = usePrevious(props.payload);
|
||||
const getAdjustedViewport = () => {
|
||||
const { width, height, formData } = props;
|
||||
@@ -79,6 +105,9 @@ export function createDeckGLComponent(
|
||||
}
|
||||
return props.viewport;
|
||||
};
|
||||
const [categories, setCategories] = useState<JsonObject>(
|
||||
getColorBreakpointsBuckets(props.formData.color_breakpoints) || [],
|
||||
);
|
||||
|
||||
const [viewport, setViewport] = useState(getAdjustedViewport());
|
||||
|
||||
@@ -90,48 +119,89 @@ export function createDeckGLComponent(
|
||||
}, []);
|
||||
|
||||
const computeLayer = useCallback(
|
||||
(props: deckGLComponentProps) => {
|
||||
const { formData, payload, onAddFilter } = props;
|
||||
(props: DeckGLComponentProps) => {
|
||||
const {
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
filterState,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
} = props;
|
||||
|
||||
return getLayer(formData, payload, onAddFilter, setTooltip) as Layer;
|
||||
return getLayer({
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}) as Layer;
|
||||
},
|
||||
[setTooltip],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const categories = getColorBreakpointsBuckets(
|
||||
props.formData.color_breakpoints,
|
||||
);
|
||||
|
||||
setCategories(categories);
|
||||
}, [props]);
|
||||
|
||||
const [layer, setLayer] = useState(computeLayer(props));
|
||||
|
||||
useEffect(() => {
|
||||
// Only recompute the layer if anything BUT the viewport has changed
|
||||
const prevFdNoVP = { ...prevFormData, viewport: null };
|
||||
const currFdNoVP = { ...props.formData, viewport: null };
|
||||
const prevFdNoVP = {
|
||||
...prevFormData,
|
||||
...prevFilterState,
|
||||
viewport: null,
|
||||
};
|
||||
const currFdNoVP = {
|
||||
...props.formData,
|
||||
...props.filterState,
|
||||
viewport: null,
|
||||
};
|
||||
if (!isEqual(prevFdNoVP, currFdNoVP) || prevPayload !== props.payload) {
|
||||
setLayer(computeLayer(props));
|
||||
}
|
||||
}, [computeLayer, prevFormData, prevPayload, props]);
|
||||
}, [computeLayer, prevFormData, prevFilterState, prevPayload, props]);
|
||||
|
||||
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>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function createCategoricalDeckGLComponent(
|
||||
getLayer: getLayerType<Layer>,
|
||||
getPoints: getPointsType,
|
||||
getLayer: GetLayerType<Layer>,
|
||||
getPoints: GetPointsType,
|
||||
) {
|
||||
return function Component(props: deckGLComponentProps) {
|
||||
return function Component(props: DeckGLComponentProps) {
|
||||
const {
|
||||
datasource,
|
||||
formData,
|
||||
@@ -140,6 +210,10 @@ export function createCategoricalDeckGLComponent(
|
||||
setControlValue,
|
||||
viewport,
|
||||
width,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -154,6 +228,10 @@ export function createCategoricalDeckGLComponent(
|
||||
getPoints={getPoints}
|
||||
width={width}
|
||||
height={height}
|
||||
setDataMask={setDataMask}
|
||||
onContextMenu={onContextMenu}
|
||||
filterState={filterState}
|
||||
emitCrossFilters={emitCrossFilters}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,16 +17,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { ArcLayer } from '@deck.gl/layers';
|
||||
import {
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
QueryFormData,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import { JsonObject, QueryFormData, t } from '@superset-ui/core';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
import { commonLayerProps } from '../common';
|
||||
import { createCategoricalDeckGLComponent } from '../../factory';
|
||||
import { GetLayerType, createCategoricalDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
@@ -60,26 +55,50 @@ function setTooltipContent(formData: QueryFormData) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
fd: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: HandlerFunction,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<ArcLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
setTooltip,
|
||||
filterState,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const data = payload.data.features;
|
||||
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(fd, setTooltip, setTooltipContent(fd)),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent: setTooltipContent(fd),
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default createCategoricalDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -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,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -25,6 +25,11 @@ import controlPanel from './controlPanel';
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Map'),
|
||||
credits: ['https://uber.github.io/deck.gl'],
|
||||
behaviors: [
|
||||
Behavior.InteractiveChart,
|
||||
Behavior.DrillBy,
|
||||
Behavior.DrillToDetail,
|
||||
],
|
||||
description: t(
|
||||
'Plot the distance (like flight paths) between origin and destination.',
|
||||
),
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Position } from '@deck.gl/core';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { createDeckGLComponent, getLayerType } from '../../factory';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import { ColorType } from '../../types';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
|
||||
@@ -39,12 +39,15 @@ function setTooltipContent(o: any) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const getLayer: getLayerType<unknown> = function (
|
||||
export const getLayer: GetLayerType<ContourLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
filterState,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
setTooltip,
|
||||
) {
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const {
|
||||
aggregation = 'SUM',
|
||||
@@ -93,7 +96,15 @@ export const getLayer: getLayerType<unknown> = function (
|
||||
getPosition: (d: { position: number[]; weight: number }) =>
|
||||
d.position as Position,
|
||||
getWeight: (d: { weight: number }) => d.weight || 0,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import transformProps from '../../transformProps';
|
||||
import controlPanel from './controlPanel';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('Spatial'), t('Comparison')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class ContourChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -23,10 +23,12 @@ import { GeoJsonLayer } from '@deck.gl/layers';
|
||||
import { Feature, Geometry, GeoJsonProperties } from 'geojson';
|
||||
import geojsonExtent from '@mapbox/geojson-extent';
|
||||
import {
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
@@ -40,6 +42,7 @@ import TooltipRow from '../../TooltipRow';
|
||||
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
import { GetLayerType } from '../../factory';
|
||||
|
||||
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
|
||||
properties: JsonObject;
|
||||
@@ -119,12 +122,15 @@ function setTooltipContent(o: JsonObject) {
|
||||
const getFillColor = (feature: JsonObject) => feature?.properties?.fillColor;
|
||||
const getLineColor = (feature: JsonObject) => feature?.properties?.strokeColor;
|
||||
|
||||
export function getLayer(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: HandlerFunction,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||
formData,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
payload,
|
||||
setTooltip,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const fc = fd.fill_color_picker;
|
||||
const sc = fd.stroke_color_picker;
|
||||
@@ -159,9 +165,17 @@ export function getLayer(
|
||||
getLineWidth: fd.line_width || 1,
|
||||
pointRadiusScale: fd.point_radius_scale,
|
||||
lineWidthUnits: fd.line_width_unit,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export type DeckGLGeoJsonProps = {
|
||||
formData: QueryFormData;
|
||||
@@ -171,6 +185,9 @@ export type DeckGLGeoJsonProps = {
|
||||
onAddFilter: HandlerFunction;
|
||||
height: number;
|
||||
width: number;
|
||||
filterState: FilterState;
|
||||
onContextMenu: HandlerFunction;
|
||||
setDataMask: SetDataMaskHook;
|
||||
};
|
||||
|
||||
export function getPoints(data: Point[]) {
|
||||
@@ -217,7 +234,15 @@ const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||
width,
|
||||
]);
|
||||
|
||||
const layer = getLayer(formData, payload, onAddFilter, setTooltip);
|
||||
const layer = getLayer({
|
||||
onContextMenu: props.onContextMenu,
|
||||
filterState: props.filterState,
|
||||
setDataMask: props.setDataMask,
|
||||
setTooltip,
|
||||
onAddFilter,
|
||||
payload,
|
||||
formData,
|
||||
});
|
||||
|
||||
return (
|
||||
<DeckGLContainerStyledWrapper
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('2D')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class GeojsonChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -16,21 +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,
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
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 } from '../../factory';
|
||||
import { createDeckGLComponent, GetLayerType } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
return (
|
||||
@@ -49,18 +47,18 @@ function setTooltipContent(o: JsonObject) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: () => void,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<GridLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
setTooltip,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
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) {
|
||||
@@ -69,22 +67,50 @@ export function getLayer(
|
||||
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,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
getColorValue: colorAggFunc,
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setDataMask,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
exampleGallery: [{ url: example }],
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('3D'), t('Comparison')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class GridChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -17,12 +17,11 @@
|
||||
* 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 { createDeckGLComponent, getLayerType } from '../../factory';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
@@ -35,12 +34,15 @@ function setTooltipContent(o: JsonObject) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const getLayer: getLayerType<unknown> = (
|
||||
export const getLayer: GetLayerType<HeatmapLayer> = ({
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
setTooltip,
|
||||
) => {
|
||||
payload,
|
||||
emitCrossFilters,
|
||||
}) => {
|
||||
const fd = formData;
|
||||
const {
|
||||
intensity = 1,
|
||||
@@ -60,10 +62,15 @@ export const getLayer: getLayerType<unknown> = (
|
||||
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,
|
||||
@@ -75,7 +82,15 @@ export const getLayer: getLayerType<unknown> = (
|
||||
getPosition: (d: { position: Position; weight: number }) => d.position,
|
||||
getWeight: (d: { position: number[]; weight: number }) =>
|
||||
d.weight ? d.weight : 1,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import transformProps from '../../transformProps';
|
||||
import controlPanel from './controlPanel';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('Spatial'), t('Comparison')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class HeatmapChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -16,21 +16,19 @@
|
||||
* 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,
|
||||
QueryFormData,
|
||||
JsonObject,
|
||||
} from '@superset-ui/core';
|
||||
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 { createDeckGLComponent } from '../../factory';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
return (
|
||||
@@ -48,18 +46,18 @@ function setTooltipContent(o: JsonObject) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: () => void,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<HexagonLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
setTooltip,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
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) {
|
||||
@@ -67,22 +65,51 @@ export function getLayer(
|
||||
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,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
getColorValue: colorAggFunc,
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('3D'), t('Geo'), t('Comparison')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class HexChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -18,12 +18,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { PathLayer } from '@deck.gl/layers';
|
||||
import { JsonObject, QueryFormData } from '@superset-ui/core';
|
||||
import { JsonObject } from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { createDeckGLComponent } from '../../factory';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
@@ -42,12 +41,15 @@ function setTooltipContent(o: JsonObject) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: () => void,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<PathLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
setTooltip,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const c = fd.color_picker;
|
||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||
@@ -72,9 +74,17 @@ export function getLayer(
|
||||
rounded: true,
|
||||
widthScale: 1,
|
||||
widthUnits: fd.line_width_unit,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
let points: Point[] = [];
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -31,6 +31,7 @@ const metadata = new ChartMetadata({
|
||||
exampleGallery: [{ url: example }],
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('Web')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class PathChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -23,20 +23,29 @@
|
||||
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ContextMenuFilters,
|
||||
ensureIsArray,
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
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';
|
||||
@@ -45,6 +54,9 @@ import {
|
||||
DeckGLContainerStyledWrapper,
|
||||
} 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
|
||||
|
||||
@@ -90,17 +102,23 @@ function setTooltipContent(formData: PolygonFormData) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
formData: PolygonFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: HandlerFunction,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
selected: JsonObject[],
|
||||
onSelect: (value: JsonValue) => void,
|
||||
) {
|
||||
const fd = formData;
|
||||
const fc = fd.fill_color_picker;
|
||||
const sc = fd.stroke_color_picker;
|
||||
export const getLayer: GetLayerType<PolygonLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
setTooltip,
|
||||
filterState,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
onSelect,
|
||||
selected,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData as PolygonFormData;
|
||||
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) {
|
||||
@@ -109,23 +127,68 @@ export function getLayer(
|
||||
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,
|
||||
number,
|
||||
]) || [0, 0, 0, 0];
|
||||
if (selected.length > 0 && !selected.includes(d[fd.line_column])) {
|
||||
if (!ensureIsArray(selected).includes(d[fd.line_column])) {
|
||||
baseColor[3] /= 2;
|
||||
}
|
||||
|
||||
@@ -146,16 +209,25 @@ export function getLayer(
|
||||
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(fd, setTooltip, tooltipContentGenerator, onSelect),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent: tooltipContentGenerator,
|
||||
onSelect,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export type PolygonFormData = QueryFormData & {
|
||||
break_points: string[];
|
||||
@@ -171,6 +243,14 @@ export type DeckGLPolygonProps = {
|
||||
onAddFilter: HandlerFunction;
|
||||
width: number;
|
||||
height: number;
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
setDataMask?: SetDataMaskHook;
|
||||
filterState?: FilterState;
|
||||
emitCrossFilters?: boolean;
|
||||
};
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
@@ -251,28 +331,35 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
);
|
||||
|
||||
const getLayers = useCallback(() => {
|
||||
const {
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
} = props;
|
||||
|
||||
if (props.payload.data.features === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const layer = getLayer(
|
||||
props.formData,
|
||||
props.payload,
|
||||
props.onAddFilter,
|
||||
const layer = getLayer({
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
selected,
|
||||
onSelect,
|
||||
);
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
});
|
||||
|
||||
return [layer];
|
||||
}, [
|
||||
onSelect,
|
||||
props.formData,
|
||||
props.onAddFilter,
|
||||
props.payload,
|
||||
selected,
|
||||
setTooltip,
|
||||
]);
|
||||
}, [onSelect, selected, setTooltip, props]);
|
||||
|
||||
const { payload, formData, setControlValue } = props;
|
||||
|
||||
@@ -281,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',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user