mirror of
https://github.com/apache/superset.git
synced 2026-07-31 11:02:27 +00:00
Compare commits
43 Commits
4.1.0rc3
...
fix_docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
774910f40d | ||
|
|
dbbcc11a98 | ||
|
|
cebd45778f | ||
|
|
1b2ecc6955 | ||
|
|
1f5e567645 | ||
|
|
c467fb566d | ||
|
|
cc0ed0fef4 | ||
|
|
4f463399a7 | ||
|
|
6264ff5165 | ||
|
|
1410e528a4 | ||
|
|
f704b0f556 | ||
|
|
bdfd5cd4ec | ||
|
|
af44b14fbe | ||
|
|
29c76ef1d5 | ||
|
|
7953c89d51 | ||
|
|
fd4c3dce44 | ||
|
|
234f8c94d1 | ||
|
|
bc2e51d8d0 | ||
|
|
2787167abe | ||
|
|
9e84e13888 | ||
|
|
cfd24e3ccd | ||
|
|
aaecec2e03 | ||
|
|
66fe0b0594 | ||
|
|
0d0b43062e | ||
|
|
72df46a729 | ||
|
|
f7cfd9182a | ||
|
|
5faaaf978b | ||
|
|
855f4c4897 | ||
|
|
008ab202f3 | ||
|
|
db311eb376 | ||
|
|
d5f33c4c02 | ||
|
|
ad82a8c14e | ||
|
|
45c18368f6 | ||
|
|
6f656914fe | ||
|
|
6706d1308f | ||
|
|
cbf1aeec7d | ||
|
|
3f7907b266 | ||
|
|
ba0d118fdd | ||
|
|
49aa74cec8 | ||
|
|
7c569abaf6 | ||
|
|
a70f2cee72 | ||
|
|
7b343f7fac | ||
|
|
742ad92189 |
21
.github/workflows/bashlib.sh
vendored
21
.github/workflows/bashlib.sh
vendored
@@ -162,7 +162,11 @@ cypress-run-all() {
|
||||
USE_DASHBOARD_FLAG='--use-dashboard'
|
||||
fi
|
||||
|
||||
python ../../scripts/cypress_run.py --parallelism $PARALLELISM --parallelism-id $PARALLEL_ID $USE_DASHBOARD_FLAG
|
||||
# UNCOMMENT the next few commands to monitor memory usage
|
||||
# monitor_memory & # Start memory monitoring in the background
|
||||
# memoryMonitorPid=$!
|
||||
python ../../scripts/cypress_run.py --parallelism $PARALLELISM --parallelism-id $PARALLEL_ID --retries 5 $USE_DASHBOARD_FLAG
|
||||
# kill $memoryMonitorPid
|
||||
|
||||
# After job is done, print out Flask log for debugging
|
||||
echo "::group::Flask log for default run"
|
||||
@@ -178,6 +182,21 @@ eyes-storybook-dependencies() {
|
||||
say "::endgroup::"
|
||||
}
|
||||
|
||||
monitor_memory() {
|
||||
# This is a small utility to monitor memory usage. Useful for debugging memory in GHA.
|
||||
# To use wrap your command as follows
|
||||
#
|
||||
# monitor_memory & # Start memory monitoring in the background
|
||||
# memoryMonitorPid=$!
|
||||
# YOUR_COMMAND_HERE
|
||||
# kill $memoryMonitorPid
|
||||
while true; do
|
||||
echo "$(date) - Top 5 memory-consuming processes:"
|
||||
ps -eo pid,comm,%mem --sort=-%mem | head -n 6 # First line is the header, next 5 are top processes
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
cypress-run-applitools() {
|
||||
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
|
||||
|
||||
|
||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@@ -32,4 +32,4 @@ jobs:
|
||||
# license: https://applitools.com/legal/open-source-terms-of-use/
|
||||
# pkg:npm/node-forge@1.3.1
|
||||
# selecting BSD-3-Clause licensing terms for node-forge to ensure compatibility with Apache
|
||||
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/applitools/core, pkg:npm/applitools/core-base, pkg:npm/applitools/css-tree, pkg:npm/applitools/ec-client, pkg:npm/applitools/eg-socks5-proxy-server, pkg:npm/applitools/eyes, pkg:npm/applitools/eyes-cypress, pkg:npm/applitools/nml-client, pkg:npm/applitools/tunnel-client, pkg:npm/applitools/utils, pkg:npm/node-forge@1.3.1
|
||||
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/applitools/core, pkg:npm/applitools/core-base, pkg:npm/applitools/css-tree, pkg:npm/applitools/ec-client, pkg:npm/applitools/eg-socks5-proxy-server, pkg:npm/applitools/eyes, pkg:npm/applitools/eyes-cypress, pkg:npm/applitools/nml-client, pkg:npm/applitools/tunnel-client, pkg:npm/applitools/utils, pkg:npm/node-forge@1.3.1, pkg:npm/rgbcolor
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
- id: set_matrix
|
||||
run: |
|
||||
MATRIX_CONFIG=$(if [ "${{ github.event_name }}" == "pull_request" ]; then echo '["dev"]'; else echo '["dev", "lean", "py310", "websocket", "dockerize"]'; fi)
|
||||
MATRIX_CONFIG=$(if [ "${{ github.event_name }}" == "pull_request" ]; then echo '["dev"]'; else echo '["dev", "lean", "py310", "websocket", "dockerize", "py311"]'; fi)
|
||||
echo "matrix_config=${MATRIX_CONFIG}" >> $GITHUB_OUTPUT
|
||||
echo $GITHUB_OUTPUT
|
||||
|
||||
|
||||
10
.github/workflows/superset-docs-verify.yml
vendored
10
.github/workflows/superset-docs-verify.yml
vendored
@@ -12,6 +12,16 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
linkinator:
|
||||
name: Link Checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: JustinBeckwith/linkinator-action@v1.10.4
|
||||
with:
|
||||
paths: "**/*.md, **/*.mdx"
|
||||
linksToSkip: '^https://github.com/apache/(superset|incubator-superset)/(pull|issue)/\d+, http://localhost:8088/, docker/.env-non-dev, http://127.0.0.1:3000/, http://localhost:9001/, https://charts.bitnami.com/bitnami, https://www.li.me/, https://www.fanatics.com/, https://tails.com/gb/, https://www.techaudit.info/, https://avetilearning.com/, https://www.udemy.com/, https://trustmedis.com/, http://theiconic.com.au/, https://dev.mysql.com/doc/refman/5.7/en/innodb-limits.html, https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fembedded-sdk?style=flat, https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-pivot-table?style=flat, https://vkusvill.ru/'
|
||||
# verbosity: 'ERROR'
|
||||
build-deploy:
|
||||
name: Build & Deploy
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
2
.github/workflows/tag-release.yml
vendored
2
.github/workflows/tag-release.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
build_preset: ["dev", "lean", "py310", "websocket", "dockerize"]
|
||||
build_preset: ["dev", "lean", "py310", "websocket", "dockerize", "py311"]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
|
||||
@@ -19,7 +19,7 @@ under the License.
|
||||
|
||||
## Change Log
|
||||
|
||||
### 4.1 (Wed Oct 16 13:07:12 2024 -0300)
|
||||
### 4.1 (Fri Nov 1 15:24:51 2024 -0700)
|
||||
|
||||
**Database Migrations**
|
||||
|
||||
@@ -31,11 +31,11 @@ under the License.
|
||||
- [#29799](https://github.com/apache/superset/pull/29799) fix: Downgrade of revision 678eefb4ab44 throws error (@michael-s-molina)
|
||||
- [#29166](https://github.com/apache/superset/pull/29166) chore: enable ruff lint rule TRY201 and B904 to improve `raise` stack traces (@mistercrunch)
|
||||
- [#28838](https://github.com/apache/superset/pull/28838) fix: Update downgrade path for migration to remove sl_tables (@sadpandajoe)
|
||||
- [#28704](https://github.com/apache/superset/pull/28704) chore: remove sl_ tables (@mistercrunch)
|
||||
- [#28704](https://github.com/apache/superset/pull/28704) chore: remove sl\_ tables (@mistercrunch)
|
||||
- [#28482](https://github.com/apache/superset/pull/28482) fix: Update migration logic in #27119 (@john-bodley)
|
||||
- [#28556](https://github.com/apache/superset/pull/28556) fix: db migration revision (@justinpark)
|
||||
- [#28416](https://github.com/apache/superset/pull/28416) feat: add support for catalogs (@betodealmeida)
|
||||
- [#27718](https://github.com/apache/superset/pull/27718) refactor(plugins): BigNumber Time Comparison with existing time_offset API (@Antonio-RiveroMartnez)
|
||||
- [#27718](https://github.com/apache/superset/pull/27718) refactor(plugins): BigNumber Time Comparison with existing time_offset API (@Antonio-RiveroMartnez)
|
||||
- [#26327](https://github.com/apache/superset/pull/26327) feat: Customizable email subject name (@puridach-w)
|
||||
- [#28422](https://github.com/apache/superset/pull/28422) fix: Update migration logic in #27119 (@john-bodley)
|
||||
- [#28394](https://github.com/apache/superset/pull/28394) feat: catalog support for Databricks native (@betodealmeida)
|
||||
@@ -71,7 +71,7 @@ under the License.
|
||||
- [#29498](https://github.com/apache/superset/pull/29498) feat: Enable customizing the docker admin password (@c-w)
|
||||
- [#29187](https://github.com/apache/superset/pull/29187) feat(dashboard): add API endpoints for generating and downloading screenshots (@eulloa10)
|
||||
- [#27221](https://github.com/apache/superset/pull/27221) feat(CLI command): Apache Superset "Factory Reset" CLI command #27207 (@mknadh)
|
||||
- [#29328](https://github.com/apache/superset/pull/29328) feat: Add Ant Design 5 Theme (@geido)
|
||||
- [#29328](https://github.com/apache/superset/pull/29328) feat: Add Ant Design 5 Theme (@geido)
|
||||
- [#29351](https://github.com/apache/superset/pull/29351) feat(e2e): implementing Cypress Dashboard on `master` branch merges (@rusackas)
|
||||
- [#29361](https://github.com/apache/superset/pull/29361) feat: Adds chart IDs option to migrate-viz (@michael-s-molina)
|
||||
- [#29329](https://github.com/apache/superset/pull/29329) feat: Adds the ECharts Sankey chart (@michael-s-molina)
|
||||
@@ -136,7 +136,7 @@ under the License.
|
||||
- [#27536](https://github.com/apache/superset/pull/27536) feat: Adds option to disable drill to detail per database (@michael-s-molina)
|
||||
- [#27571](https://github.com/apache/superset/pull/27571) feat(supersetbot): label PRs and issues with author's public org (@mistercrunch)
|
||||
- [#27542](https://github.com/apache/superset/pull/27542) feat(maps): Add Italy regions code to the map generator notebook (@iskenderulgen)
|
||||
- [#27524](https://github.com/apache/superset/pull/27524) feat(plugins): add color options for big number with time comparison (@lilykuang)
|
||||
- [#27524](https://github.com/apache/superset/pull/27524) feat(plugins): add color options for big number with time comparison (@lilykuang)
|
||||
- [#27455](https://github.com/apache/superset/pull/27455) feat: Add Turkey's regions to country map visualization (@iskenderulgen)
|
||||
- [#27046](https://github.com/apache/superset/pull/27046) feat(supersetbot): introduce `supersetbot` as its own npm package, CLI and comment-operated bot (@mistercrunch)
|
||||
- [#27255](https://github.com/apache/superset/pull/27255) feat: show more information when loading chart (@betodealmeida)
|
||||
@@ -157,6 +157,16 @@ under the License.
|
||||
|
||||
**Fixes**
|
||||
|
||||
- [#30819](https://github.com/apache/superset/pull/30819) fix(plugin-chart-echarts): sort tooltip correctly (@villebro)
|
||||
- [#30755](https://github.com/apache/superset/pull/30755) fix(Dashboard): Sync/Async Dashboard Screenshot Generation and Default Cache (@geido)
|
||||
- [#30773](https://github.com/apache/superset/pull/30773) fix: catalog migration w/o connection (@betodealmeida)
|
||||
- [#30429](https://github.com/apache/superset/pull/30429) fix: CI remove cypress command --headed (@mistercrunch)
|
||||
- [#30735](https://github.com/apache/superset/pull/30735) fix(Jinja): Extra cache keys for calculated columns and metrics using Jinja (@Vitor-Avila)
|
||||
- [#30699](https://github.com/apache/superset/pull/30699) fix: Nested transaction is inactive when embedding dashboard (@michael-s-molina)
|
||||
- [#30675](https://github.com/apache/superset/pull/30675) fix(dashboard): Include `urlParams` in the screenshot generation (@Vitor-Avila)
|
||||
- [#30715](https://github.com/apache/superset/pull/30715) fix(Jinja): Extra cache keys for Jinja columns (@geido)
|
||||
- [#30680](https://github.com/apache/superset/pull/30680) fix(chart): Table and page entries misaligned (@justinpark)
|
||||
- [#30348](https://github.com/apache/superset/pull/30348) fix(explore): Missing markarea component broke annotations in echarts (@kgabryje)
|
||||
- [#30628](https://github.com/apache/superset/pull/30628) fix: First item hovered on stacked bar (@michael-s-molina)
|
||||
- [#30617](https://github.com/apache/superset/pull/30617) fix(docs): address two linkinator failures (@sfirke)
|
||||
- [#30438](https://github.com/apache/superset/pull/30438) fix(Filters): Apply native & cross filters on common columns (@geido)
|
||||
@@ -165,7 +175,7 @@ under the License.
|
||||
- [#30565](https://github.com/apache/superset/pull/30565) fix: update html rendering to true from false (@sadpandajoe)
|
||||
- [#30202](https://github.com/apache/superset/pull/30202) fix: adhoc metrics (@betodealmeida)
|
||||
- [#30549](https://github.com/apache/superset/pull/30549) fix(Jinja): Extra cache keys to consider vars with set (@geido)
|
||||
- [#30425](https://github.com/apache/superset/pull/30425) fix(dashboard-export): Fixes datasetId is not replaced with datasetUuid in Dashboard export in 4.1.x (@fmannhardt)
|
||||
- [#30425](https://github.com/apache/superset/pull/30425) fix(dashboard-export): Fixes datasetId is not replaced with datasetUuid in Dashboard export in 4.1.x (@fmannhardt)
|
||||
- [#30563](https://github.com/apache/superset/pull/30563) fix: Horizon Chart are not working any more (@michael-s-molina)
|
||||
- [#30564](https://github.com/apache/superset/pull/30564) fix: Incorrect type in config.py (@michael-s-molina)
|
||||
- [#30560](https://github.com/apache/superset/pull/30560) fix: Unable to parse escaped tables (@michael-s-molina)
|
||||
@@ -380,12 +390,12 @@ under the License.
|
||||
- [#28241](https://github.com/apache/superset/pull/28241) fix(explore): temporal column mixin (@justinpark)
|
||||
- [#28156](https://github.com/apache/superset/pull/28156) fix(sqllab): invalid css scope for ace editor autocomplete (@justinpark)
|
||||
- [#28222](https://github.com/apache/superset/pull/28222) fix: Dremio alias (@betodealmeida)
|
||||
- [#28152](https://github.com/apache/superset/pull/28152) fix(sql_parse): Provide more lenient logic when extracting latest[_sub]_partition (@john-bodley)
|
||||
- [#28152](https://github.com/apache/superset/pull/28152) fix(sql_parse): Provide more lenient logic when extracting latest[_sub]\_partition (@john-bodley)
|
||||
- [#28226](https://github.com/apache/superset/pull/28226) fix(maps): adds Crimea back to Ukraine 🇺🇦 (@rusackas)
|
||||
- [#28197](https://github.com/apache/superset/pull/28197) fix: Remove deprecated ignoreTestFiles from Applitools Cypress (@geido)
|
||||
- [#28189](https://github.com/apache/superset/pull/28189) fix(docs): ERD docs fail on master (@mistercrunch)
|
||||
- [#27554](https://github.com/apache/superset/pull/27554) fix(AlertsReports): making log retention "None" option valid (@fisjac)
|
||||
- [#28117](https://github.com/apache/superset/pull/28117) fix(sql_parse): Support Jinja format() filter when extracting latest[_sub]_partition (@john-bodley)
|
||||
- [#27554](https://github.com/apache/superset/pull/27554) fix(AlertsReports): making log retention "None" option valid (@fisjac)
|
||||
- [#28117](https://github.com/apache/superset/pull/28117) fix(sql_parse): Support Jinja format() filter when extracting latest[_sub]\_partition (@john-bodley)
|
||||
- [#27195](https://github.com/apache/superset/pull/27195) fix: Upgrade eyes-cypress to latest (@geido)
|
||||
- [#28061](https://github.com/apache/superset/pull/28061) fix: switch off dependabot for pip/python (@mistercrunch)
|
||||
- [#28054](https://github.com/apache/superset/pull/28054) fix(Dashboard): Support "Edit chart" click on a new window (@geido)
|
||||
@@ -455,6 +465,11 @@ under the License.
|
||||
|
||||
**Others**
|
||||
|
||||
- [#30729](https://github.com/apache/superset/pull/30729) chore: bump werkzeug to address vulnerability (@dpgaspar)
|
||||
- [#30733](https://github.com/apache/superset/pull/30733) ci: Add Python 3.11 images to Docker Hub (@padbk)
|
||||
- [#30397](https://github.com/apache/superset/pull/30397) chore: alter scripts/cypress_run to run one file per command + retry (@mistercrunch)
|
||||
- [#30354](https://github.com/apache/superset/pull/30354) chore: split cypress files for less memory (@eschutho)
|
||||
- [#30719](https://github.com/apache/superset/pull/30719) chore(Dashboard): Simplify scoping logic for cross/native filters (@geido)
|
||||
- [#29937](https://github.com/apache/superset/pull/29937) chore: Update to Dockerfile to get creating releases to work (@sadpandajoe)
|
||||
- [#29874](https://github.com/apache/superset/pull/29874) perf: Implement Echarts treeshaking (@kgabryje)
|
||||
- [#26257](https://github.com/apache/superset/pull/26257) chore(chart-controls): migrate enzyme to RTL (@justinpark)
|
||||
@@ -556,7 +571,7 @@ under the License.
|
||||
- [#28876](https://github.com/apache/superset/pull/28876) chore(sqllab): Add logging for actions (@justinpark)
|
||||
- [#29245](https://github.com/apache/superset/pull/29245) test(storybook): fix component stories (@msyavuz)
|
||||
- [#29235](https://github.com/apache/superset/pull/29235) chore: Remove the need for explicit bubble up of certain exceptions (@john-bodley)
|
||||
- [#28628](https://github.com/apache/superset/pull/28628) chore: Set isolation level to READ COMMITTED for testing et al. (@john-bodley)
|
||||
- [#28628](https://github.com/apache/superset/pull/28628) chore: Set isolation level to READ COMMITTED for testing et al. (@john-bodley)
|
||||
- [#29108](https://github.com/apache/superset/pull/29108) refactor(sqllab): nonblocking switch query editor (@justinpark)
|
||||
- [#29232](https://github.com/apache/superset/pull/29232) build(deps-dev): bump braces from 3.0.2 to 3.0.3 in /superset-embedded-sdk (@dependabot[bot])
|
||||
- [#29226](https://github.com/apache/superset/pull/29226) chore(intros): Update INTHEWILD.md (@RIS3cz)
|
||||
@@ -818,7 +833,7 @@ under the License.
|
||||
- [#27977](https://github.com/apache/superset/pull/27977) chore(docs): removing Superset Community Newsletter archive (@rusackas)
|
||||
- [#27975](https://github.com/apache/superset/pull/27975) chore(docs): adding ASF Privacy Link. (@rusackas)
|
||||
- [#27954](https://github.com/apache/superset/pull/27954) docs(k8s): making it clear users MUST update secrets for prod instances. (@rusackas)
|
||||
- [#27810](https://github.com/apache/superset/pull/27810) build(deps-dev): update @types/mapbox__geojson-extent requirement from ^1.0.0 to ^1.0.3 in /superset-frontend/plugins/legacy-preset-chart-deckgl (@dependabot[bot])
|
||||
- [#27810](https://github.com/apache/superset/pull/27810) build(deps-dev): update @types/mapbox\_\_geojson-extent requirement from ^1.0.0 to ^1.0.3 in /superset-frontend/plugins/legacy-preset-chart-deckgl (@dependabot[bot])
|
||||
- [#27946](https://github.com/apache/superset/pull/27946) chore(helm): bumping app version to 4.0.0 in helm chart (@lodu)
|
||||
- [#27149](https://github.com/apache/superset/pull/27149) chore(tests): Remove ineffectual login (@john-bodley)
|
||||
- [#27937](https://github.com/apache/superset/pull/27937) chore: Adds 4.0.0 data to CHANGELOG.md and UPDATING.md (@michael-s-molina)
|
||||
|
||||
50
CHANGELOG/4.1.1.md
Normal file
50
CHANGELOG/4.1.1.md
Normal file
@@ -0,0 +1,50 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
## Change Log
|
||||
|
||||
### 4.1 (Fri Nov 15 22:13:57 2024 +0530)
|
||||
|
||||
**Database Migrations**
|
||||
|
||||
**Features**
|
||||
|
||||
**Fixes**
|
||||
|
||||
- [#30886](https://github.com/apache/superset/pull/30886) fix: blocks UI elements on right side (@samarsrivastav)
|
||||
- [#30859](https://github.com/apache/superset/pull/30859) fix(package.json): Pin luxon version to unblock master (@geido)
|
||||
- [#30588](https://github.com/apache/superset/pull/30588) fix(explore): column data type tooltip format (@mistercrunch)
|
||||
- [#29911](https://github.com/apache/superset/pull/29911) fix: Rename database from 'couchbasedb' to 'couchbase' in documentation and db_engine_specs (@ayush-couchbase)
|
||||
- [#30828](https://github.com/apache/superset/pull/30828) fix(TimezoneSelector): Failing unit tests due to timezone change (@geido)
|
||||
- [#30875](https://github.com/apache/superset/pull/30875) fix: don't show metadata for embedded dashboards (@sadpandajoe)
|
||||
- [#30851](https://github.com/apache/superset/pull/30851) fix: Graph chart colors (@michael-s-molina)
|
||||
- [#29867](https://github.com/apache/superset/pull/29867) fix(capitalization): Capitalizing a button. (@rusackas)
|
||||
- [#29782](https://github.com/apache/superset/pull/29782) fix(translations): Translate embedded errors (@rusackas)
|
||||
- [#29772](https://github.com/apache/superset/pull/29772) fix: Fixing incomplete string escaping. (@rusackas)
|
||||
- [#29725](https://github.com/apache/superset/pull/29725) fix(frontend/docker, ci): fix borked Docker build due to Lerna v8 uplift (@hainenber)
|
||||
|
||||
**Others**
|
||||
|
||||
- [#30576](https://github.com/apache/superset/pull/30576) chore: add link to Superset when report error (@eschutho)
|
||||
- [#29786](https://github.com/apache/superset/pull/29786) refactor(Slider): Upgrade Slider to Antd 5 (@geido)
|
||||
- [#29674](https://github.com/apache/superset/pull/29674) refactor(ChartCreation): Migrate tests to RTL (@rtexelm)
|
||||
- [#29843](https://github.com/apache/superset/pull/29843) refactor(controls): Migrate AdhocMetricOption.test to RTL (@rtexelm)
|
||||
- [#29845](https://github.com/apache/superset/pull/29845) refactor(controls): Migrate MetricDefinitionValue.test to RTL (@rtexelm)
|
||||
- [#28424](https://github.com/apache/superset/pull/28424) docs: Check markdown files for bad links using linkinator (@rusackas)
|
||||
- [#29768](https://github.com/apache/superset/pull/29768) docs(contributing): fix broken link to translations sub-section (@sfirke)
|
||||
@@ -94,9 +94,9 @@ This statement thanks the following, on which it draws for content and inspirati
|
||||
|
||||
* [CouchDB Project Code of conduct](http://couchdb.apache.org/conduct.html)
|
||||
* [Fedora Project Code of Conduct](http://fedoraproject.org/code-of-conduct)
|
||||
* [Speak Up! Code of Conduct](http://speakup.io/coc.html)
|
||||
* [Speak Up! Code of Conduct](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html)
|
||||
* [Django Code of Conduct](https://www.djangoproject.com/conduct/)
|
||||
* [Debian Code of Conduct](http://www.debian.org/vote/2014/vote_002)
|
||||
* [Debian Code of Conduct](https://www.debian.org/vote/2014/vote_002)
|
||||
* [Twitter Open Source Code of Conduct](https://github.com/twitter/code-of-conduct/blob/master/code-of-conduct.md)
|
||||
* [Mozilla Code of Conduct/Draft](https://wiki.mozilla.org/Code_of_Conduct/Draft#Conflicts_of_Interest)
|
||||
* [Python Diversity Appendix](https://www.python.org/community/diversity/)
|
||||
|
||||
@@ -57,10 +57,11 @@ RUN npm run build-translation
|
||||
RUN rm /app/superset/translations/*/LC_MESSAGES/*.po
|
||||
RUN rm /app/superset/translations/messages.pot
|
||||
|
||||
FROM python:${PY_VER} AS python-base
|
||||
######################################################################
|
||||
# Final lean image...
|
||||
######################################################################
|
||||
FROM python:${PY_VER} AS lean
|
||||
FROM python-base AS lean
|
||||
|
||||
WORKDIR /app
|
||||
ENV LANG=C.UTF-8 \
|
||||
|
||||
@@ -505,7 +505,7 @@ We also need to update the Environment section of [ISSUE_TEMPLATE/bug-report.yml
|
||||
|
||||
Docker release with proper tags should happen automatically as version
|
||||
tags get pushed to the `apache/superset` GitHub repository through this
|
||||
[GitHub action](https://github.com/apache/superset/blob/master/.github/workflows/docker-release.yml)
|
||||
[GitHub action](https://github.com/apache/superset/blob/master/.github/workflows/docker.yml)
|
||||
|
||||
Note that this GH action implements a `workflow_dispatch` trigger,
|
||||
meaning that it can be triggered manually from the GitHub UI. If anything
|
||||
|
||||
@@ -102,7 +102,7 @@ Some of the new features in this release are disabled by default. Each has a fea
|
||||
This release includes **hundreds** of bugfixes and stability enhancements. Future major releases will have a continued emphasis on providing a stable and bug-free experience for the user.
|
||||
|
||||
# PR Highlights
|
||||
Below is a highlight of the PRs included in this update. The full list is much longer, and can be found [here](apache/incubator-superset/CHANGELOG.md).
|
||||
Below is a highlight of the PRs included in this update. The full list is much longer, and can be found [here](https://github.com/apache/superset/blob/master/CHANGELOG.md).
|
||||
|
||||
## User Experience
|
||||
- Revert "refactor: Remove usages of reactable from TimeTable (#11046)" (#[11150](https://github.com/apache/incubator-superset/pull/11150))
|
||||
@@ -222,4 +222,4 @@ Below is a highlight of the PRs included in this update. The full list is much l
|
||||
## Complete Changelog
|
||||
Backwards incompatible changes and can be found [here](../../UPDATING.md).
|
||||
|
||||
To see the complete changelog, see [apache/incubator-superset/CHANGELOG.md](https://github.com/apache/superset/blob/master/CHANGELOG.md)
|
||||
To see the complete changelog, see [apache/superset/CHANGELOG.md](https://github.com/apache/superset/blob/master/CHANGELOG.md)
|
||||
|
||||
@@ -137,6 +137,6 @@ when available.
|
||||
**Changelog**
|
||||
|
||||
To see the complete changelog in this release, head to
|
||||
[CHANGELOG.MD](https://github.com/apache/superset/blob/1.5/CHANGELOG/1.5.0.md).
|
||||
[CHANGELOG.MD](https://github.com/apache/superset/blob/master/CHANGELOG/1.5.0.md).
|
||||
As mentioned earlier, this release has a MASSIVE amount of bug fixes. The full
|
||||
changelog lists all of them!
|
||||
|
||||
@@ -34,7 +34,7 @@ We released a [Big Number with Time Period Comparison](https://github.com/apache
|
||||
</div>
|
||||
|
||||
### Table with Time Comparison
|
||||
Added functionality to do [table time comparisons](https://github.com/apache/superset/pull/28057) behind the `CHART_PLUGINS_EXPERIMENTAL` feature flag. This will help improve and facilitate efficient data analysis.
|
||||
Added functionality to do [table time comparisons](https://github.com/apache/superset/pull/28057). This will help improve and facilitate efficient data analysis.
|
||||
|
||||
<div>
|
||||
<image src="media/table_with_time.png" alt="Image" width="100%">
|
||||
|
||||
@@ -45,7 +45,7 @@ These features are **finished** but currently being tested. They are usable, but
|
||||
- CACHE_IMPERSONATION
|
||||
- CONFIRM_DASHBOARD_DIFF
|
||||
- DRILL_TO_DETAIL
|
||||
- DYNAMIC_PLUGINS: [(docs)](https://superset.apache.org/docs/configuration/running-on-kubernetes)
|
||||
- DYNAMIC_PLUGINS
|
||||
- ENABLE_SUPERSET_META_DB: [(docs)](https://superset.apache.org/docs/configuration/databases/#querying-across-databases)
|
||||
- ESTIMATE_QUERY_COST
|
||||
- GLOBAL_ASYNC_QUERIES [(docs)](https://github.com/apache/superset/blob/master/CONTRIBUTING.md#async-chart-queries)
|
||||
@@ -77,7 +77,7 @@ independently. This new framework will also allow for non-boolean configurations
|
||||
|
||||
- ALERTS_ATTACH_REPORTS
|
||||
- ALLOW_ADHOC_SUBQUERY
|
||||
- DASHBOARD_RBAC [(docs)](https://superset.apache.org/docs/using-superset/first-dashboard#manage-access-to-dashboards)
|
||||
- DASHBOARD_RBAC [(docs)](https://superset.apache.org/docs/using-superset/creating-your-first-dashboard#manage-access-to-dashboards)
|
||||
- DATAPANEL_CLOSED_BY_DEFAULT
|
||||
- DRUID_JOINS
|
||||
- EMBEDDABLE_CHARTS
|
||||
|
||||
@@ -25,16 +25,17 @@ all you have to do is file a simple PR [like this one](https://github.com/apache
|
||||
the categorization is inaccurate, please file a PR with your correction as well.
|
||||
Join our growing community!
|
||||
|
||||
|
||||
### Sharing Economy
|
||||
|
||||
- [Airbnb](https://github.com/airbnb)
|
||||
- [Faasos](http://faasos.com/) [@shashanksingh]
|
||||
- [Hostnfly](https://www.hostnfly.com/) [@alexisrosuel]
|
||||
- [Lime](https://www.limebike.com/) [@cxmcc]
|
||||
- [Lime](https://www.li.me/) [@cxmcc]
|
||||
- [Lyft](https://www.lyft.com/)
|
||||
- [Ontruck](https://www.ontruck.com/)
|
||||
|
||||
### Financial Services
|
||||
|
||||
- [Aktia Bank plc](https://www.aktia.com) [@villebro]
|
||||
- [American Express](https://www.americanexpress.com) [@TheLastSultan]
|
||||
- [Cape Crypto](https://capecrypto.com)
|
||||
@@ -47,15 +48,16 @@ Join our growing community!
|
||||
- [bumper](https://www.bumper.co/) [@vasu-ram, @JamiePercival]
|
||||
|
||||
### Gaming
|
||||
- [Digit Game Studios](https://www.digitgaming.com/)
|
||||
|
||||
- [Popoko VM Games Studio](https://popoko.live)
|
||||
|
||||
### E-Commerce
|
||||
|
||||
- [AiHello](https://www.aihello.com) [@ganeshkrishnan1]
|
||||
- [Bazaar Technologies](https://www.bazaartech.com) [@umair-abro]
|
||||
- [Dragonpass](https://www.dragonpass.com.cn/) [@zhxjdwh]
|
||||
- [Dropit Shopping](https://www.dropit.shop/) [@dropit-dev]
|
||||
- [Fanatics](https://www.fanatics.com) [@coderfender]
|
||||
- [Fanatics](https://www.fanatics.com/) [@coderfender]
|
||||
- [Fordeal](http://www.fordeal.com) [@Renkai]
|
||||
- [GFG - Global Fashion Group](https://global-fashion-group.com) [@ksaagariconic]
|
||||
- [HuiShouBao](http://www.huishoubao.com/) [@Yukinoshita-Yukino]
|
||||
@@ -64,14 +66,15 @@ Join our growing community!
|
||||
- [Rakuten Viki](https://www.viki.com)
|
||||
- [Shopee](https://shopee.sg) [@xiaohanyu]
|
||||
- [Shopkick](https://www.shopkick.com) [@LAlbertalli]
|
||||
- [Tails.com](https://tails.com) [@alanmcruickshank]
|
||||
- [Tails.com](https://tails.com/gb/) [@alanmcruickshank]
|
||||
- [THE ICONIC](http://theiconic.com.au/) [@ksaagariconic]
|
||||
- [Utair](https://www.utair.ru) [@utair-digital]
|
||||
- [VkusVill](https://www.vkusvill.ru) [@ETselikov]
|
||||
- [VkusVill](https://vkusvill.ru/) [@ETselikov]
|
||||
- [Zalando](https://www.zalando.com) [@dmigo]
|
||||
- [Zalora](https://www.zalora.com) [@ksaagariconic]
|
||||
|
||||
### Enterprise Technology
|
||||
|
||||
- [A3Data](https://a3data.com.br) [@neylsoncrepalde]
|
||||
- [Analytics Aura](https://analyticsaura.com/) [@Analytics-Aura]
|
||||
- [Apollo GraphQL](https://www.apollographql.com/) [@evans]
|
||||
@@ -80,6 +83,7 @@ Join our growing community!
|
||||
- [Caizin](https://caizin.com/) [@tejaskatariya]
|
||||
- [Careem](https://www.careem.com/) [@SamraHanifCareem]
|
||||
- [Cloudsmith](https://cloudsmith.io) [@alancarson]
|
||||
- [CnOvit](https://www.cnovit.com/) [@xieshaohu]
|
||||
- [Cyberhaven](https://www.cyberhaven.com/) [@toliver-ch]
|
||||
- [Deepomatic](https://deepomatic.com/) [@Zanoellia]
|
||||
- [Dial Once](https://www.dial-once.com/)
|
||||
@@ -88,8 +92,7 @@ Join our growing community!
|
||||
- [Endress+Hauser](http://www.endress.com/) [@rumbin]
|
||||
- [FBK - ICT center](http://ict.fbk.eu)
|
||||
- [Gavagai](https://gavagai.io) [@gavagai-corp]
|
||||
- [GfK Data Lab](http://datalab.gfk.com) [@mherr]
|
||||
- [GrowthSimple](https://growthsimple.ai/)
|
||||
- [GfK Data Lab](https://www.gfk.com/home) [@mherr]
|
||||
- [Hydrolix](https://www.hydrolix.io/)
|
||||
- [Intercom](https://www.intercom.com/) [@kate-gallo]
|
||||
- [jampp](https://jampp.com/)
|
||||
@@ -104,17 +107,14 @@ Join our growing community!
|
||||
- [Peak AI](https://www.peak.ai/) [@azhar22k]
|
||||
- [PeopleDoc](https://www.people-doc.com) [@rodo]
|
||||
- [Preset, Inc.](https://preset.io)
|
||||
- [Pronto Tools](http://www.prontotools.io) [@zkan]
|
||||
- [PubNub](https://pubnub.com) [@jzucker2]
|
||||
- [ReadyTech](https://www.readytech.io)
|
||||
- [Reward Gateway](https://www.rewardgateway.com)
|
||||
- [ScopeAI](https://www.getscopeai.com) [@iloveluce]
|
||||
- [Showmax](https://tech.showmax.com) [@bobek]
|
||||
- [source{d}](https://www.sourced.tech) [@marnovo]
|
||||
- [Steamroot](https://streamroot.io/)
|
||||
- [Showmax](https://showmax.com) [@bobek]
|
||||
- [TechAudit](https://www.techaudit.info) [@ETselikov]
|
||||
- [Tenable](https://www.tenable.com) [@dflionis]
|
||||
- [Tentacle](https://public.tentaclecmi.com) [@jdclarke5]
|
||||
- [Tentacle](https://tentaclecmi.com) [@jdclarke5]
|
||||
- [timbr.ai](https://timbr.ai/) [@semantiDan]
|
||||
- [Tobii](http://www.tobii.com/) [@dwa]
|
||||
- [Tooploox](https://www.tooploox.com/) [@jakubczaplicki]
|
||||
@@ -124,6 +124,7 @@ Join our growing community!
|
||||
- [Zeta](https://www.zeta.tech/) [@shaikidris]
|
||||
|
||||
### Media & Entertainment
|
||||
|
||||
- [6play](https://www.6play.fr) [@CoryChaplin]
|
||||
- [bilibili](https://www.bilibili.com) [@Moinheart]
|
||||
- [BurdaForward](https://www.burda-forward.de/en/)
|
||||
@@ -136,6 +137,7 @@ Join our growing community!
|
||||
- [Zaihang](http://www.zaih.com/)
|
||||
|
||||
### Education
|
||||
|
||||
- [Aveti Learning](https://avetilearning.com/) [@TheShubhendra]
|
||||
- [Brilliant.org](https://brilliant.org/)
|
||||
- [Platzi.com](https://platzi.com/)
|
||||
@@ -146,6 +148,7 @@ Join our growing community!
|
||||
- [WikiMedia Foundation](https://wikimediafoundation.org) [@vg]
|
||||
|
||||
### Energy
|
||||
|
||||
- [Airboxlab](https://foobot.io) [@antoine-galataud]
|
||||
- [DouroECI](https://www.douroeci.com/) [@nunohelibeires]
|
||||
- [Safaricom](https://www.safaricom.co.ke/) [@mmutiso]
|
||||
@@ -153,31 +156,35 @@ Join our growing community!
|
||||
- [Wattbewerb](https://wattbewerb.de/) [@wattbewerb]
|
||||
|
||||
### Healthcare
|
||||
|
||||
- [Amino](https://amino.com) [@shkr]
|
||||
- [Beans](https://www.beans.fi) [@kakoni]
|
||||
- [Bluesquare](https://www.bluesquarehub.com/) [@madewulf]
|
||||
- [Care](https://www.getcare.io/)[@alandao2021]
|
||||
- [Living Goods](https://www.livinggoods.org) [@chelule]
|
||||
- [Maieutical Labs](https://maieuticallabs.it) [@xrmx]
|
||||
- [QPID Health](http://www.qpidhealth.com/)
|
||||
- [REDCap Cloud](https://www.redcapcloud.com/)
|
||||
- [TrustMedis](https://trustmedis.com) [@famasya]
|
||||
- [TrustMedis](https://trustmedis.com/) [@famasya]
|
||||
- [WeSure](https://www.wesure.cn/)
|
||||
|
||||
### HR / Staffing
|
||||
|
||||
- [Swile](https://www.swile.co/) [@PaoloTerzi]
|
||||
- [Symmetrics](https://www.symmetrics.fyi)
|
||||
- [bluquist](https://bluquist.com/)
|
||||
|
||||
### Government
|
||||
|
||||
- [City of Ann Arbor, MI](https://www.a2gov.org/) [@sfirke]
|
||||
- [RIS3 Strategy of CZ, MIT CR](https://www.ris3.cz/) [@RIS3CZ]
|
||||
|
||||
### Travel
|
||||
|
||||
- [Agoda](https://www.agoda.com/) [@lostseaway, @maiake, @obombayo]
|
||||
- [Skyscanner](https://www.skyscanner.net/) [@cleslie, @stanhoucke]
|
||||
|
||||
### Others
|
||||
|
||||
- [10Web](https://10web.io/)
|
||||
- [AI inside](https://inside.ai/en/)
|
||||
- [Automattic](https://automattic.com/) [@Khrol, @Usiel]
|
||||
|
||||
@@ -228,7 +228,8 @@ assists people when migrating to a new version.
|
||||
- [19273](https://github.com/apache/superset/pull/19273): The `SUPERSET_CELERY_WORKERS` and `SUPERSET_WORKERS` config keys has been removed. Configure Celery directly using `CELERY_CONFIG` on Superset.
|
||||
- [19231](https://github.com/apache/superset/pull/19231): The `ENABLE_REACT_CRUD_VIEWS` feature flag has been removed (permanently enabled). Any deployments which had set this flag to false will need to verify that the React views support their use case.
|
||||
- [19230](https://github.com/apache/superset/pull/19230): The `ROW_LEVEL_SECURITY` feature flag has been removed (permanently enabled). Any deployments which had set this flag to false will need to verify that the presence of the Row Level Security feature does not interfere with their use case.
|
||||
- [19168](https://github.com/apache/superset/pull/19168): Celery upgrade to 5.X resulted in breaking changes to its command line invocation. Please follow [these](https://docs.celeryq.dev/en/stable/whatsnew-5.2.html#step-1-adjust-your-command-line-invocation) instructions for adjustments. Also consider migrating you Celery config per [here](https://docs.celeryq.dev/en/stable/userguide/configuration.html#conf-old-settings-map).
|
||||
- [19168](https://github.com/apache/superset/pull/19168): Celery upgrade to 5.X resulted in breaking changes to its command line invocation.
|
||||
html#step-1-adjust-your-command-line-invocation) instructions for adjustments. Also consider migrating you Celery config per [here](https://docs.celeryq.dev/en/stable/userguide/configuration.html#conf-old-settings-map).
|
||||
- [19142](https://github.com/apache/superset/pull/19142): The `VERSIONED_EXPORT` config key is now `True` by default.
|
||||
- [19113](https://github.com/apache/superset/pull/19113): The `ENABLE_JAVASCRIPT_CONTROLS` config key has moved from an app config to a feature flag. Any deployments who overrode this setting will now need to override the feature flag from here onward.
|
||||
- [19107](https://github.com/apache/superset/pull/19107): The `SQLLAB_BACKEND_PERSISTENCE` feature flag is now `True` by default, which enables persisting SQL Lab tabs in the backend instead of the browser's `localStorage`.
|
||||
@@ -539,7 +540,7 @@ assists people when migrating to a new version.
|
||||
- [8117](https://github.com/apache/superset/pull/8117): If you are
|
||||
using `ENABLE_PROXY_FIX = True`, review the newly-introduced variable,
|
||||
`PROXY_FIX_CONFIG`, which changes the proxy behavior in accordance with
|
||||
[Werkzeug](https://werkzeug.palletsprojects.com/en/0.15.x/middleware/proxy_fix/)
|
||||
Werkzeug.
|
||||
|
||||
- [8069](https://github.com/apache/superset/pull/8069): introduces
|
||||
[MessagePack](https://github.com/msgpack/msgpack-python) and
|
||||
|
||||
@@ -26,7 +26,7 @@ fi
|
||||
if [ "$BUILD_SUPERSET_FRONTEND_IN_DOCKER" = "true" ]; then
|
||||
cd /app/superset-frontend
|
||||
npm install -f --no-optional --global webpack webpack-cli
|
||||
npm install -f --no-optional
|
||||
npm install -f
|
||||
|
||||
echo "Running frontend"
|
||||
npm run dev
|
||||
|
||||
@@ -54,7 +54,7 @@ are compatible with Superset.
|
||||
| [Azure MS SQL](/docs/configuration/databases#sql-server) | `pip install pymssql` | `mssql+pymssql://UserName@presetSQL:TestPassword@presetSQL.database.windows.net:1433/TestSchema` |
|
||||
| [ClickHouse](/docs/configuration/databases#clickhouse) | `pip install clickhouse-connect` | `clickhousedb://{username}:{password}@{hostname}:{port}/{database}` |
|
||||
| [CockroachDB](/docs/configuration/databases#cockroachdb) | `pip install cockroachdb` | `cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable` |
|
||||
| [CouchbaseDB](/docs/configuration/databases#couchbaseDB) | `pip install couchbase-sqlalchemy` | `couchbasedb://{username}:{password}@{hostname}:{port}?truststorepath={ssl certificate path}` |
|
||||
| [Couchbase](/docs/configuration/databases#couchbase) | `pip install couchbase-sqlalchemy` | `couchbase://{username}:{password}@{hostname}:{port}?truststorepath={ssl certificate path}` |
|
||||
| [Dremio](/docs/configuration/databases#dremio) | `pip install sqlalchemy_dremio` | `dremio://user:pwd@host:31010/` |
|
||||
| [Elasticsearch](/docs/configuration/databases#elasticsearch) | `pip install elasticsearch-dbapi` | `elasticsearch+http://{user}:{password}@{host}:9200/` |
|
||||
| [Exasol](/docs/configuration/databases#exasol) | `pip install sqlalchemy-exasol` | `exa+pyodbc://{username}:{password}@{hostname}:{port}/my_schema?CONNECTIONLCALL=en_US.UTF-8&driver=EXAODBC` |
|
||||
@@ -375,9 +375,10 @@ cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable
|
||||
|
||||
|
||||
|
||||
#### CouchbaseDB
|
||||
#### Couchbase
|
||||
|
||||
The recommended connector library for CouchbaseDB is
|
||||
The Couchbase's Superset connection is designed to support two services: Couchbase Analytics and Couchbase Columnar.
|
||||
The recommended connector library for couchbase is
|
||||
[couchbase-sqlalchemy](https://github.com/couchbase/couchbase-sqlalchemy).
|
||||
```
|
||||
pip install couchbase-sqlalchemy
|
||||
@@ -386,7 +387,7 @@ pip install couchbase-sqlalchemy
|
||||
The expected connection string is formatted as follows:
|
||||
|
||||
```
|
||||
couchbasedb://{username}:{password}@{hostname}:{port}?truststorepath={certificate path}?ssl={true/false}
|
||||
couchbase://{username}:{password}@{hostname}:{port}?truststorepath={certificate path}?ssl={true/false}
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ text strings from Superset's UI. You can jump into the existing
|
||||
language dictionaries at
|
||||
`superset/translations/<language_code>/LC_MESSAGES/messages.po`, or
|
||||
even create a dictionary for a new language altogether.
|
||||
See [Translating](howtos#contribute-translations) for more details.
|
||||
See [Translating](howtos#contributing-translations) for more details.
|
||||
|
||||
### Ask Questions
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ A philosophy we would like to strongly encourage is
|
||||
|
||||
The purpose is to separate problem from possible solutions.
|
||||
|
||||
**Bug fixes:** If you’re only fixing a small bug, it’s fine to submit a pull request right away but we highly recommend to file an issue detailing what you’re fixing. This is helpful in case we don’t accept that specific fix but want to keep track of the issue. Please keep in mind that the project maintainers reserve the rights to accept or reject incoming PRs, so it is better to separate the issue and the code to fix it from each other. In some cases, project maintainers may request you to create a separate issue from PR before proceeding.
|
||||
**Bug fixes:** If you’re only fixing a small bug, it’s fine to submit a pull request right away but we highly recommend filing an issue detailing what you’re fixing. This is helpful in case we don’t accept that specific fix but want to keep track of the issue. Please keep in mind that the project maintainers reserve the rights to accept or reject incoming PRs, so it is better to separate the issue and the code to fix it from each other. In some cases, project maintainers may request you to create a separate issue from PR before proceeding.
|
||||
|
||||
**Refactor:** For small refactors, it can be a standalone PR itself detailing what you are refactoring and why. If there are concerns, project maintainers may request you to create a `#SIP` for the PR before proceeding.
|
||||
|
||||
|
||||
@@ -380,7 +380,7 @@ vine==5.1.0
|
||||
# kombu
|
||||
wcwidth==0.2.13
|
||||
# via prompt-toolkit
|
||||
werkzeug==3.0.3
|
||||
werkzeug==3.0.6
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# flask
|
||||
|
||||
@@ -16,26 +16,18 @@
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
XVFB_PRE_CMD = "xvfb-run --auto-servernum --server-args='-screen 0, 1024x768x24' "
|
||||
REPO = os.getenv("GITHUB_REPOSITORY") or "apache/superset"
|
||||
GITHUB_EVENT_NAME = os.getenv("GITHUB_REPOSITORY") or "push"
|
||||
GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME") or "push"
|
||||
CYPRESS_RECORD_KEY = os.getenv("CYPRESS_RECORD_KEY") or ""
|
||||
|
||||
|
||||
def compute_hash(file_path: str) -> str:
|
||||
return hashlib.md5(file_path.encode()).hexdigest()
|
||||
|
||||
|
||||
def compute_group_index(hash_value: str, num_groups: int) -> int:
|
||||
return int(hash_value, 16) % num_groups
|
||||
|
||||
|
||||
def generate_build_id() -> str:
|
||||
"""Generates a build ID based on the current timestamp."""
|
||||
now = datetime.now()
|
||||
rounded_minute = now.minute - (now.minute % 20)
|
||||
rounded_time = now.replace(minute=rounded_minute, second=0, microsecond=0)
|
||||
@@ -44,42 +36,70 @@ def generate_build_id() -> str:
|
||||
)
|
||||
|
||||
|
||||
def get_cypress_cmd(
|
||||
spec_list: list[str], _filter: str, group: str, use_dashboard: bool
|
||||
) -> str:
|
||||
def run_cypress_for_test_file(
|
||||
test_file: str, retries: int, use_dashboard: bool, group: str, dry_run: bool
|
||||
) -> int:
|
||||
"""Runs Cypress for a single test file and retries upon failure."""
|
||||
cypress_cmd = "./node_modules/.bin/cypress run"
|
||||
|
||||
os.environ["TERM"] = "xterm"
|
||||
os.environ["ELECTRON_DISABLE_GPU"] = "true"
|
||||
build_id = generate_build_id()
|
||||
browser = os.getenv("CYPRESS_BROWSER", "chrome")
|
||||
chrome_flags = "--disable-dev-shm-usage"
|
||||
|
||||
# Create Cypress command for a single test file
|
||||
if use_dashboard:
|
||||
# Run using cypress.io service
|
||||
spec: str = "cypress/e2e/*/**/*"
|
||||
cmd = (
|
||||
f"{XVFB_PRE_CMD} "
|
||||
f'{cypress_cmd} --spec "{spec}" --browser {browser} '
|
||||
f'{cypress_cmd} --spec "{test_file}" --browser {browser} '
|
||||
f"--record --group {group} --tag {REPO},{GITHUB_EVENT_NAME} "
|
||||
f"--parallel --ci-build-id {build_id}"
|
||||
f"--parallel --ci-build-id {build_id} "
|
||||
f"-- {chrome_flags}"
|
||||
)
|
||||
else:
|
||||
# Run local, but split the execution
|
||||
os.environ.pop("CYPRESS_RECORD_KEY", None)
|
||||
spec_list_str = ",".join(sorted(spec_list))
|
||||
if _filter:
|
||||
spec_list_str = ",".join(sorted([s for s in spec_list if _filter in s]))
|
||||
cmd = (
|
||||
f"{XVFB_PRE_CMD} "
|
||||
f"{cypress_cmd} --browser {browser} "
|
||||
f'--spec "{spec_list_str}" '
|
||||
f'--spec "{test_file}" '
|
||||
f"-- {chrome_flags}"
|
||||
)
|
||||
return cmd
|
||||
|
||||
if dry_run:
|
||||
# Print the command instead of executing it
|
||||
print(f"DRY RUN: {cmd}")
|
||||
return 0
|
||||
|
||||
for attempt in range(retries):
|
||||
print(f"RUN: {cmd} (Attempt {attempt + 1}/{retries})")
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
# Stream stdout in real-time
|
||||
if process.stdout:
|
||||
for stdout_line in iter(process.stdout.readline, ""):
|
||||
print(stdout_line, end="")
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0:
|
||||
print(f"Test {test_file} succeeded on attempt {attempt + 1}")
|
||||
return 0
|
||||
else:
|
||||
print(f"Test {test_file} failed on attempt {attempt + 1}")
|
||||
|
||||
print(f"Test {test_file} failed after {retries} retries.")
|
||||
return process.returncode
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Cypress commands based on test file hash"
|
||||
description="Run Cypress tests with retries per test file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-dashboard",
|
||||
@@ -93,9 +113,12 @@ def main() -> None:
|
||||
"--parallelism-id", type=int, required=True, help="ID of the parallelism group"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--filter", type=str, required=False, default=None, help="filter to test"
|
||||
"--filter", type=str, required=False, default=None, help="Filter to test"
|
||||
)
|
||||
parser.add_argument("--group", type=str, default="Default", help="Group name")
|
||||
parser.add_argument(
|
||||
"--retries", type=int, default=3, help="Number of retries per test file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
@@ -109,14 +132,17 @@ def main() -> None:
|
||||
cypress_tests_path = os.path.join(cypress_base_full_path, "cypress/e2e")
|
||||
|
||||
test_files = []
|
||||
file_count = 0
|
||||
for root, _, files in os.walk(cypress_tests_path):
|
||||
for file in files:
|
||||
if file.endswith("test.ts") or file.endswith("test.js"):
|
||||
file_count += 1
|
||||
test_files.append(
|
||||
os.path.join(root, file).replace(cypress_base_full_path, "")
|
||||
)
|
||||
print(f"Found {file_count} test files.")
|
||||
|
||||
# Initialize groups
|
||||
# Initialize groups for round-robin distribution
|
||||
groups: dict[int, list[str]] = {i: [] for i in range(args.parallelism)}
|
||||
|
||||
# Sort test files to ensure deterministic distribution
|
||||
@@ -127,12 +153,21 @@ def main() -> None:
|
||||
group_index = index % args.parallelism
|
||||
groups[group_index].append(test_file)
|
||||
|
||||
# Only run tests for the group that matches the parallelism ID
|
||||
group_id = args.parallelism_id
|
||||
spec_list = groups[group_id]
|
||||
cmd = get_cypress_cmd(spec_list, args.filter, args.group, args.use_dashboard)
|
||||
print(f"RUN: {cmd}")
|
||||
if not args.dry_run:
|
||||
subprocess.run(cmd, shell=True, check=True, stdout=None, stderr=None)
|
||||
|
||||
# Run each test file independently with retry logic or dry-run
|
||||
processed_file_count: int = 0
|
||||
for test_file in spec_list:
|
||||
result = run_cypress_for_test_file(
|
||||
test_file, args.retries, args.use_dashboard, args.group, args.dry_run
|
||||
)
|
||||
if result != 0:
|
||||
print(f"Exiting due to failure in {test_file}")
|
||||
exit(result)
|
||||
processed_file_count += 1
|
||||
print(f"Ran {processed_file_count} test files successfully.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd ../.. && pwd )"
|
||||
LICENSE_TMP=$(mktemp)
|
||||
@@ -47,6 +48,10 @@ pybabel extract \
|
||||
--copyright-holder=Superset \
|
||||
--project=Superset \
|
||||
-k _ -k __ -k t -k tn:1,2 -k tct .
|
||||
|
||||
# Normalize .pot file
|
||||
msgcat --sort-by-msgid --no-wrap --no-location superset/translations/messages.pot -o superset/translations/messages.pot
|
||||
|
||||
cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \
|
||||
&& mv messages.pot.tmp superset/translations/messages.pot
|
||||
|
||||
|
||||
@@ -16,10 +16,19 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { SAMPLE_DASHBOARD_1, TABBED_DASHBOARD } from 'cypress/utils/urls';
|
||||
import {
|
||||
SAMPLE_DASHBOARD_1,
|
||||
SUPPORTED_CHARTS_DASHBOARD,
|
||||
TABBED_DASHBOARD,
|
||||
} from 'cypress/utils/urls';
|
||||
import { drag, resize, waitForChartLoad } from 'cypress/utils';
|
||||
import * as ace from 'brace';
|
||||
import { interceptGet, interceptUpdate, openTab } from './utils';
|
||||
import {
|
||||
interceptExploreUpdate,
|
||||
interceptGet,
|
||||
interceptUpdate,
|
||||
openTab,
|
||||
} from './utils';
|
||||
import {
|
||||
interceptExploreJson,
|
||||
interceptFiltering as interceptCharts,
|
||||
@@ -42,15 +51,37 @@ function openProperties() {
|
||||
cy.getBySel('header-actions-menu')
|
||||
.contains('Edit properties')
|
||||
.click({ force: true });
|
||||
cy.wait(500);
|
||||
cy.get('.ant-modal-body').should('be.visible');
|
||||
});
|
||||
}
|
||||
|
||||
function openExploreProperties() {
|
||||
cy.getBySel('actions-trigger').click({ force: true });
|
||||
cy.get('.ant-dropdown-menu')
|
||||
.contains('Edit chart properties')
|
||||
.click({ force: true });
|
||||
cy.get('.ant-modal-body').should('be.visible');
|
||||
}
|
||||
|
||||
function assertMetadata(text: string) {
|
||||
const regex = new RegExp(text);
|
||||
cy.get('#json_metadata')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
const metadata = cy.$$('#json_metadata')[0];
|
||||
|
||||
// cypress can read this locally, but not in ci
|
||||
// so we have to use the ace module directly to fetch the value
|
||||
expect(ace.edit(metadata).getValue()).to.match(regex);
|
||||
});
|
||||
}
|
||||
|
||||
function openAdvancedProperties() {
|
||||
cy.get('.ant-modal-body')
|
||||
.contains('Advanced')
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
cy.get('#json_metadata').should('be.visible');
|
||||
}
|
||||
|
||||
function dragComponent(
|
||||
@@ -83,20 +114,36 @@ function visitEdit(sampleDashboard = SAMPLE_DASHBOARD_1) {
|
||||
cy.visit(sampleDashboard);
|
||||
cy.wait('@get');
|
||||
editDashboard();
|
||||
cy.get('.grid-container').should('exist');
|
||||
cy.wait('@filtering');
|
||||
cy.wait(500);
|
||||
}
|
||||
|
||||
function resetTabbedDashboard(go = false) {
|
||||
function visit(sampleDashboard = SAMPLE_DASHBOARD_1) {
|
||||
interceptCharts();
|
||||
interceptGet();
|
||||
|
||||
if (sampleDashboard === SAMPLE_DASHBOARD_1) {
|
||||
cy.createSampleDashboards([0]);
|
||||
}
|
||||
|
||||
cy.visit(sampleDashboard);
|
||||
cy.wait('@get');
|
||||
cy.get('.grid-container').should('exist');
|
||||
cy.wait(500);
|
||||
}
|
||||
|
||||
function resetDashboardColors(dashboard = 'tabbed_dash') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
cy.getDashboard('tabbed_dash').then((r: Record<string, any>) => {
|
||||
cy.getDashboard(dashboard).then((r: Record<string, any>) => {
|
||||
const jsonMetadata = r?.json_metadata || '{}';
|
||||
const metadata = JSON.parse(jsonMetadata);
|
||||
const resetMetadata = JSON.stringify({
|
||||
...metadata,
|
||||
color_scheme: '',
|
||||
label_colors: {},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
map_label_colors: {},
|
||||
});
|
||||
cy.updateDashboard(r.id, {
|
||||
certification_details: r.certification_details,
|
||||
@@ -106,25 +153,35 @@ function resetTabbedDashboard(go = false) {
|
||||
json_metadata: resetMetadata,
|
||||
owners: r.owners,
|
||||
slug: r.slug,
|
||||
}).then(() => {
|
||||
if (go) {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function visitResetTabbedDashboard() {
|
||||
resetTabbedDashboard(true);
|
||||
function selectColorScheme(
|
||||
color: string,
|
||||
target = 'dashboard-edit-properties-form',
|
||||
) {
|
||||
cy.get(`[data-test="${target}"] input[aria-label="Select color scheme"]`)
|
||||
.first()
|
||||
.then($input => {
|
||||
cy.wrap($input).click({ force: true });
|
||||
cy.wrap($input).type(color.slice(0, 5), { force: true });
|
||||
});
|
||||
cy.getBySel(color).click({ force: true });
|
||||
}
|
||||
|
||||
function selectColorScheme(color: string) {
|
||||
cy.get(
|
||||
'[data-test="dashboard-edit-properties-form"] [aria-label="Select color scheme"]',
|
||||
)
|
||||
.first()
|
||||
.click();
|
||||
cy.getBySel(color).click({ force: true });
|
||||
function saveAndGo(dashboard = 'Tabbed Dashboard') {
|
||||
interceptExploreUpdate();
|
||||
cy.getBySel('query-save-button').click();
|
||||
cy.getBySel('save-modal-body').then($modal => {
|
||||
cy.wrap($modal)
|
||||
.find("div[aria-label='Select a dashboard'] .ant-select-selection-item")
|
||||
.should('have.text', dashboard);
|
||||
cy.getBySel('save-overwrite-radio').should('not.be.disabled');
|
||||
cy.getBySel('save-overwrite-radio').click();
|
||||
cy.get('#btn_modal_save_goto_dash').click();
|
||||
cy.wait('@chartUpdate');
|
||||
});
|
||||
}
|
||||
|
||||
function applyChanges() {
|
||||
@@ -137,37 +194,37 @@ function saveChanges() {
|
||||
cy.wait('@update');
|
||||
}
|
||||
|
||||
function assertMetadata(text: string) {
|
||||
const regex = new RegExp(text);
|
||||
cy.get('#json_metadata')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
const metadata = cy.$$('#json_metadata')[0];
|
||||
|
||||
// cypress can read this locally, but not in ci
|
||||
// so we have to use the ace module directly to fetch the value
|
||||
expect(ace.edit(metadata).getValue()).to.match(regex);
|
||||
});
|
||||
}
|
||||
function clearMetadata() {
|
||||
cy.get('#json_metadata').then($jsonmetadata => {
|
||||
cy.wrap($jsonmetadata).find('.ace_content').click();
|
||||
cy.wrap($jsonmetadata).find('.ace_content').click({ force: true });
|
||||
cy.wrap($jsonmetadata)
|
||||
.find('.ace_text-input')
|
||||
.type('{selectall} {backspace}', { force: true });
|
||||
.then($ace => {
|
||||
cy.wrap($ace).focus();
|
||||
cy.wrap($ace).should('have.focus');
|
||||
cy.wrap($ace).type('{selectall}', { force: true });
|
||||
cy.wrap($ace).type('{backspace}', { force: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeMetadata(metadata: string) {
|
||||
cy.get('#json_metadata').then($jsonmetadata =>
|
||||
cy
|
||||
.wrap($jsonmetadata)
|
||||
cy.get('#json_metadata').then($jsonmetadata => {
|
||||
cy.wrap($jsonmetadata).find('.ace_content').click({ force: true });
|
||||
cy.wrap($jsonmetadata)
|
||||
.find('.ace_text-input')
|
||||
.type(metadata, { parseSpecialCharSequences: false, force: true }),
|
||||
);
|
||||
.then($ace => {
|
||||
cy.wrap($ace).focus();
|
||||
cy.wrap($ace).should('have.focus');
|
||||
cy.wrap($ace).type(metadata, {
|
||||
parseSpecialCharSequences: false,
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openExplore(chartName: string) {
|
||||
function openExploreWithDashboardContext(chartName: string) {
|
||||
interceptExploreJson();
|
||||
interceptGet();
|
||||
|
||||
@@ -181,23 +238,351 @@ function openExplore(chartName: string) {
|
||||
.should('contain', 'Edit chart')
|
||||
.click();
|
||||
cy.wait('@getJson');
|
||||
cy.get('.chart-container').should('exist');
|
||||
}
|
||||
|
||||
function saveExploreColorScheme(
|
||||
chart = 'Top 10 California Names Timeseries',
|
||||
colorScheme = 'supersetColors',
|
||||
) {
|
||||
interceptExploreUpdate();
|
||||
openExploreWithDashboardContext(chart);
|
||||
openTab(0, 1, 'control-tabs');
|
||||
selectColorScheme(colorScheme, 'control-item');
|
||||
cy.getBySel('query-save-button').click();
|
||||
cy.getBySel('save-overwrite-radio').click();
|
||||
cy.getBySel('btn-modal-save').click();
|
||||
cy.wait('@chartUpdate');
|
||||
}
|
||||
|
||||
describe('Dashboard edit', () => {
|
||||
describe('Color consistency', () => {
|
||||
beforeEach(() => {
|
||||
visitResetTabbedDashboard();
|
||||
resetDashboardColors();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
resetTabbedDashboard();
|
||||
it('should not allow to change color scheme of a chart when dashboard has one', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
openTab(0, 1, 'control-tabs');
|
||||
|
||||
cy.get('[aria-label="Select color scheme"]').should('be.disabled');
|
||||
});
|
||||
|
||||
it('should not allow to change color scheme of a chart when dashboard has no scheme but chart has shared labels', () => {
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// open second top tab to catch shared labels
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Trends',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
openTab(0, 0);
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
openTab(0, 1, 'control-tabs');
|
||||
|
||||
cy.get('[aria-label="Select color scheme"]').should('be.disabled');
|
||||
});
|
||||
|
||||
it('should allow to change color scheme of a chart when dashboard has no scheme but only custom label colors', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
writeMetadata('{"color_scheme":"","label_colors":{"Anthony":"red"}}');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
openTab(0, 1, 'control-tabs');
|
||||
selectColorScheme('blueToGreen', 'control-item');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
// label Christopher
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// label Daniel
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(0, 76, 218)');
|
||||
|
||||
// label David
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.eq(3)
|
||||
.should('have.css', 'fill', 'rgb(0, 116, 241)');
|
||||
});
|
||||
|
||||
it('should allow to change color scheme of a chart when dashboard has no scheme and show the change', () => {
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
openTab(0, 1, 'control-tabs');
|
||||
selectColorScheme('blueToGreen', 'control-item');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
saveAndGo();
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// reset original scheme
|
||||
saveExploreColorScheme();
|
||||
});
|
||||
|
||||
it('should allow to change color scheme of a chart when dashboard has no scheme but custom label colors and show the change', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
writeMetadata('{"color_scheme":"","label_colors":{"Anthony":"red"}}');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
openTab(0, 1, 'control-tabs');
|
||||
selectColorScheme('blueToGreen', 'control-item');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
// label Christopher
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
saveAndGo();
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
// label Christopher
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// reset original scheme
|
||||
saveExploreColorScheme();
|
||||
});
|
||||
|
||||
it('should not change colors on refreshes with no color scheme set', () => {
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Andrew
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(69, 78, 124)');
|
||||
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Andrew
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(69, 78, 124)');
|
||||
});
|
||||
|
||||
it('should not change colors on refreshes with color scheme set', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Andrew
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(0, 76, 218)');
|
||||
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Andrew
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(0, 76, 218)');
|
||||
});
|
||||
|
||||
it('should respect chart color scheme when none is set for the dashboard', () => {
|
||||
openProperties();
|
||||
cy.get('[aria-label="Select color scheme"]').should('have.value', '');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
@@ -214,7 +599,8 @@ describe('Dashboard edit', () => {
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
});
|
||||
|
||||
it('should apply same color to same labels with color scheme set', () => {
|
||||
it('should apply same color to same labels with color scheme set on refresh', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
@@ -238,17 +624,63 @@ describe('Dashboard edit', () => {
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
visit(TABBED_DASHBOARD);
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
});
|
||||
|
||||
it('should apply same color to same labels with no color scheme set', () => {
|
||||
openProperties();
|
||||
cy.get('[aria-label="Select color scheme"]').should('have.value', '');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
it('should apply same color to same labels with no color scheme set on refresh', () => {
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
@@ -275,6 +707,7 @@ describe('Dashboard edit', () => {
|
||||
});
|
||||
|
||||
it('custom label colors should take the precedence in nested tabs', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
@@ -305,6 +738,7 @@ describe('Dashboard edit', () => {
|
||||
});
|
||||
|
||||
it('label colors should take the precedence for rendered charts in nested tabs', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
// open the tab first time and let chart load
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
@@ -333,6 +767,7 @@ describe('Dashboard edit', () => {
|
||||
});
|
||||
|
||||
it('should re-apply original color after removing custom label color with color scheme set', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
@@ -375,6 +810,7 @@ describe('Dashboard edit', () => {
|
||||
});
|
||||
|
||||
it('should re-apply original color after removing custom label color with no color scheme set', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
@@ -438,6 +874,7 @@ describe('Dashboard edit', () => {
|
||||
});
|
||||
|
||||
it('should show the same colors in Explore', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
@@ -461,7 +898,7 @@ describe('Dashboard edit', () => {
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
openExplore('Top 10 California Names Timeseries');
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
@@ -469,7 +906,8 @@ describe('Dashboard edit', () => {
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
it.skip('should change color scheme multiple times', () => {
|
||||
it('should change color scheme multiple times', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
@@ -487,7 +925,7 @@ describe('Dashboard edit', () => {
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
@@ -496,7 +934,7 @@ describe('Dashboard edit', () => {
|
||||
// label Anthony
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
editDashboard();
|
||||
openProperties();
|
||||
@@ -507,7 +945,7 @@ describe('Dashboard edit', () => {
|
||||
// label Anthony
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(41, 105, 107)');
|
||||
.should('have.css', 'fill', 'rgb(0, 128, 246)');
|
||||
|
||||
// open main tab and nested tab
|
||||
openTab(0, 0);
|
||||
@@ -518,10 +956,11 @@ describe('Dashboard edit', () => {
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(41, 105, 107)');
|
||||
.should('have.css', 'fill', 'rgb(0, 128, 246)');
|
||||
});
|
||||
|
||||
it.skip('should apply the color scheme across main tabs', () => {
|
||||
it('should apply the color scheme across main tabs', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
@@ -533,10 +972,11 @@ describe('Dashboard edit', () => {
|
||||
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
});
|
||||
|
||||
it.skip('should apply the color scheme across main tabs for rendered charts', () => {
|
||||
it('should apply the color scheme across main tabs for rendered charts', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
waitForChartLoad({ name: 'Treemap', viz: 'treemap_v2' });
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
@@ -549,7 +989,7 @@ describe('Dashboard edit', () => {
|
||||
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(41, 105, 107)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// change scheme now that charts are rendered across the main tabs
|
||||
editDashboard();
|
||||
@@ -560,10 +1000,11 @@ describe('Dashboard edit', () => {
|
||||
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(0, 128, 246)');
|
||||
});
|
||||
|
||||
it.skip('should apply the color scheme in nested tabs', () => {
|
||||
it('should apply the color scheme in nested tabs', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
@@ -579,17 +1020,18 @@ describe('Dashboard edit', () => {
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// open another nested tab
|
||||
openTab(2, 1);
|
||||
waitForChartLoad({ name: 'Growth Rate', viz: 'line' });
|
||||
cy.get('[data-test-chart-name="Growth Rate"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
});
|
||||
|
||||
it.skip('should apply a valid color scheme for rendered charts in nested tabs', () => {
|
||||
it('should apply a valid color scheme for rendered charts in nested tabs', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
// open the tab first time and let chart load
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
@@ -611,7 +1053,7 @@ describe('Dashboard edit', () => {
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -623,9 +1065,10 @@ describe('Dashboard edit', () => {
|
||||
beforeEach(() => {
|
||||
cy.createSampleDashboards([0]);
|
||||
openProperties();
|
||||
selectColorScheme('supersetColors');
|
||||
});
|
||||
|
||||
it.skip('should accept a valid color scheme', () => {
|
||||
it('should accept a valid color scheme', () => {
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
writeMetadata('{"color_scheme":"lyftColors"}');
|
||||
@@ -636,21 +1079,21 @@ describe('Dashboard edit', () => {
|
||||
applyChanges();
|
||||
});
|
||||
|
||||
it.skip('should overwrite the color scheme when advanced is closed', () => {
|
||||
it('should overwrite the color scheme when advanced is closed', () => {
|
||||
selectColorScheme('blueToGreen');
|
||||
openAdvancedProperties();
|
||||
assertMetadata('blueToGreen');
|
||||
applyChanges();
|
||||
});
|
||||
|
||||
it.skip('should overwrite the color scheme when advanced is open', () => {
|
||||
it('should overwrite the color scheme when advanced is open', () => {
|
||||
openAdvancedProperties();
|
||||
selectColorScheme('modernSunset');
|
||||
assertMetadata('modernSunset');
|
||||
applyChanges();
|
||||
});
|
||||
|
||||
it.skip('should not accept an invalid color scheme', () => {
|
||||
it('should not accept an invalid color scheme', () => {
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
// allow console error
|
||||
@@ -714,7 +1157,7 @@ describe('Dashboard edit', () => {
|
||||
visitEdit();
|
||||
});
|
||||
|
||||
it.skip('should add charts', () => {
|
||||
it('should add charts', () => {
|
||||
cy.get('[role="checkbox"]').click();
|
||||
dragComponent();
|
||||
cy.getBySel('dashboard-component-chart-holder').should('have.length', 1);
|
||||
@@ -762,7 +1205,7 @@ describe('Dashboard edit', () => {
|
||||
visitEdit();
|
||||
});
|
||||
|
||||
it.skip('should save', () => {
|
||||
it('should save', () => {
|
||||
cy.get('[role="checkbox"]').click();
|
||||
dragComponent();
|
||||
cy.getBySel('header-save-button').should('be.enabled');
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* 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 { nativeFilters } from 'cypress/support/directories';
|
||||
|
||||
import {
|
||||
addCountryNameFilter,
|
||||
applyNativeFilterValueWithIndex,
|
||||
enterNativeFilterEditModal,
|
||||
inputNativeFilterDefaultValue,
|
||||
saveNativeFilterSettings,
|
||||
validateFilterNameOnDashboard,
|
||||
testItems,
|
||||
interceptFilterState,
|
||||
} from './utils';
|
||||
import {
|
||||
prepareDashboardFilters,
|
||||
SAMPLE_CHART,
|
||||
visitDashboard,
|
||||
} from './shared_dashboard_functions';
|
||||
|
||||
function openMoreFilters(waitFilterState = true) {
|
||||
interceptFilterState();
|
||||
cy.getBySel('dropdown-container-btn').click();
|
||||
|
||||
if (waitFilterState) {
|
||||
cy.wait('@postFilterState');
|
||||
}
|
||||
}
|
||||
|
||||
function openVerticalFilterBar() {
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
cy.getBySel('filter-bar__expand-button').click();
|
||||
}
|
||||
|
||||
function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') {
|
||||
cy.getBySel('filterbar-orientation-icon').click();
|
||||
cy.wait(250);
|
||||
cy.getBySel('dropdown-selectable-icon-submenu')
|
||||
.contains('Orientation of filter bar')
|
||||
.should('exist')
|
||||
.trigger('mouseover');
|
||||
|
||||
if (orientation === 'vertical') {
|
||||
cy.get('.ant-dropdown-menu-item-selected')
|
||||
.contains('Horizontal (Top)')
|
||||
.should('exist');
|
||||
cy.get('.ant-dropdown-menu-item').contains('Vertical (Left)').click();
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
} else {
|
||||
cy.get('.ant-dropdown-menu-item-selected')
|
||||
.contains('Vertical (Left)')
|
||||
.should('exist');
|
||||
cy.get('.ant-dropdown-menu-item').contains('Horizontal (Top)').click();
|
||||
cy.getBySel('loading-indicator').should('exist');
|
||||
cy.getBySel('filter-bar').should('exist');
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
}
|
||||
}
|
||||
|
||||
describe('Horizontal FilterBar', () => {
|
||||
it('should go from vertical to horizontal and the opposite', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
setFilterBarOrientation('vertical');
|
||||
});
|
||||
|
||||
it('should show all default actions in horizontal mode', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.getBySel('horizontal-filterbar-empty')
|
||||
.contains('No filters are currently added to this dashboard.')
|
||||
.should('exist');
|
||||
cy.getBySel('filter-bar__create-filter').should('exist');
|
||||
cy.getBySel('filterbar-action-buttons').should('exist');
|
||||
});
|
||||
|
||||
it('should stay in horizontal mode when reloading', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.reload();
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show all filters in available space on load', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.get('.filter-item-wrapper').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should show "more filters" on window resizing up and down', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
cy.viewport(768, 1024);
|
||||
cy.getBySel('form-item-value').should('have.length', 0);
|
||||
openMoreFilters(false);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
|
||||
cy.getBySel('filter-bar').click();
|
||||
cy.viewport(1000, 1024);
|
||||
openMoreFilters(false);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
|
||||
cy.getBySel('filter-bar').click();
|
||||
cy.viewport(1300, 1024);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
cy.getBySel('dropdown-container-btn').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show "more filters" and scroll', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.get('.filter-item-wrapper').should('have.length', 3);
|
||||
openMoreFilters();
|
||||
cy.getBySel('form-item-value').should('have.length', 12);
|
||||
cy.getBySel('filter-control-name').contains('test_10').should('be.visible');
|
||||
cy.getBySel('filter-control-name')
|
||||
.contains('test_12')
|
||||
.should('not.be.visible');
|
||||
cy.get('.ant-popover-inner-content').scrollTo('bottom');
|
||||
cy.getBySel('filter-control-name').contains('test_12').should('be.visible');
|
||||
});
|
||||
|
||||
it('should display newly added filter', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
enterNativeFilterEditModal(false);
|
||||
addCountryNameFilter();
|
||||
saveNativeFilterSettings([]);
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it('should spot changes in "more filters" and apply their values', () => {
|
||||
cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
openMoreFilters();
|
||||
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.wait('@chart');
|
||||
cy.get('.antd5-scroll-number.antd5-badge-count').should(
|
||||
'have.attr',
|
||||
'title',
|
||||
'1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should focus filter and open "more filters" programmatically', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
openMoreFilters();
|
||||
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.getBySel('slice-header').within(() => {
|
||||
cy.get('.filter-counts').trigger('mouseover');
|
||||
});
|
||||
cy.get('.filterStatusPopover').contains('test_9').click();
|
||||
cy.getBySel('dropdown-content').should('be.visible');
|
||||
cy.get('.ant-select-focused').should('be.visible');
|
||||
});
|
||||
|
||||
it('should show tag count and one plain tag on focus and only count on blur in select ', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue('Albania');
|
||||
cy.get('.ant-select-selection-search-input').clear({ force: true });
|
||||
inputNativeFilterDefaultValue('Algeria', true);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.getBySel('filter-bar').within(() => {
|
||||
cy.get(nativeFilters.filterItem).contains('Albania').should('be.visible');
|
||||
cy.get(nativeFilters.filterItem).contains('+ 1 ...').should('be.visible');
|
||||
cy.get('.ant-select-selection-search-input').click();
|
||||
cy.get(nativeFilters.filterItem).contains('+ 2 ...').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,15 +16,19 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import qs from 'querystring';
|
||||
import {
|
||||
dashboardView,
|
||||
nativeFilters,
|
||||
exploreView,
|
||||
dataTestChartName,
|
||||
exploreView,
|
||||
nativeFilters,
|
||||
} from 'cypress/support/directories';
|
||||
import { SAMPLE_DASHBOARD_1 } from 'cypress/utils/urls';
|
||||
import qs from 'querystring';
|
||||
|
||||
import {
|
||||
prepareDashboardFilters,
|
||||
SAMPLE_CHART,
|
||||
visitDashboard,
|
||||
} from './shared_dashboard_functions';
|
||||
import {
|
||||
addCountryNameFilter,
|
||||
addParentFilterWithValue,
|
||||
@@ -40,147 +44,16 @@ import {
|
||||
fillNativeFilterForm,
|
||||
getNativeFilterPlaceholderWithIndex,
|
||||
inputNativeFilterDefaultValue,
|
||||
saveNativeFilterSettings,
|
||||
nativeFilterTooltips,
|
||||
saveNativeFilterSettings,
|
||||
testItems,
|
||||
undoDeleteNativeFilter,
|
||||
validateFilterContentOnDashboard,
|
||||
valueNativeFilterOptions,
|
||||
validateFilterNameOnDashboard,
|
||||
testItems,
|
||||
valueNativeFilterOptions,
|
||||
WORLD_HEALTH_CHARTS,
|
||||
interceptGet,
|
||||
interceptCharts,
|
||||
interceptDatasets,
|
||||
interceptFilterState,
|
||||
} from './utils';
|
||||
|
||||
const SAMPLE_CHART = { name: 'Most Populated Countries', viz: 'table' };
|
||||
|
||||
function visitDashboard(createSample = true) {
|
||||
interceptCharts();
|
||||
interceptGet();
|
||||
interceptDatasets();
|
||||
|
||||
if (createSample) {
|
||||
cy.createSampleDashboards([0]);
|
||||
}
|
||||
|
||||
cy.visit(SAMPLE_DASHBOARD_1);
|
||||
cy.wait('@get');
|
||||
cy.wait('@getCharts');
|
||||
cy.wait('@getDatasets');
|
||||
cy.url().should('contain', 'native_filters_key');
|
||||
}
|
||||
|
||||
function prepareDashboardFilters(
|
||||
filters: { name: string; column: string; datasetId: number }[],
|
||||
) {
|
||||
cy.createSampleDashboards([0]);
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `api/v1/dashboard/1-sample-dashboard`,
|
||||
}).then(res => {
|
||||
const { body } = res;
|
||||
const dashboardId = body.result.id;
|
||||
const allFilters: Record<string, unknown>[] = [];
|
||||
filters.forEach((f, i) => {
|
||||
allFilters.push({
|
||||
id: `NATIVE_FILTER-fLH0pxFQ${i}`,
|
||||
controlValues: {
|
||||
enableEmptyFilter: false,
|
||||
defaultToFirstItem: false,
|
||||
multiSelect: true,
|
||||
searchAllOptions: false,
|
||||
inverseSelection: false,
|
||||
},
|
||||
name: f.name,
|
||||
filterType: 'filter_select',
|
||||
targets: [
|
||||
{
|
||||
datasetId: f.datasetId,
|
||||
column: { name: f.column },
|
||||
},
|
||||
],
|
||||
defaultDataMask: {
|
||||
extraFormData: {},
|
||||
filterState: {},
|
||||
ownState: {},
|
||||
},
|
||||
cascadeParentIds: [],
|
||||
scope: {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: [],
|
||||
},
|
||||
type: 'NATIVE_FILTER',
|
||||
description: '',
|
||||
chartsInScope: [5],
|
||||
tabsInScope: [],
|
||||
});
|
||||
});
|
||||
if (dashboardId) {
|
||||
const jsonMetadata = {
|
||||
native_filter_configuration: allFilters,
|
||||
timed_refresh_immune_slices: [],
|
||||
expanded_slices: {},
|
||||
refresh_frequency: 0,
|
||||
color_scheme: '',
|
||||
label_colors: {},
|
||||
shared_label_colors: {},
|
||||
color_scheme_domain: [],
|
||||
cross_filters_enabled: false,
|
||||
positions: {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
|
||||
GRID_ID: {
|
||||
type: 'GRID',
|
||||
id: 'GRID_ID',
|
||||
children: ['ROW-0rHnUz4nMA'],
|
||||
parents: ['ROOT_ID'],
|
||||
},
|
||||
HEADER_ID: {
|
||||
id: 'HEADER_ID',
|
||||
type: 'HEADER',
|
||||
meta: { text: '1 - Sample dashboard' },
|
||||
},
|
||||
'CHART-DF6EfI55F-': {
|
||||
type: 'CHART',
|
||||
id: 'CHART-DF6EfI55F-',
|
||||
children: [],
|
||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-0rHnUz4nMA'],
|
||||
meta: {
|
||||
width: 4,
|
||||
height: 50,
|
||||
chartId: 5,
|
||||
sliceName: 'Most Populated Countries',
|
||||
},
|
||||
},
|
||||
'ROW-0rHnUz4nMA': {
|
||||
type: 'ROW',
|
||||
id: 'ROW-0rHnUz4nMA',
|
||||
children: ['CHART-DF6EfI55F-'],
|
||||
parents: ['ROOT_ID', 'GRID_ID'],
|
||||
meta: { background: 'BACKGROUND_TRANSPARENT' },
|
||||
},
|
||||
},
|
||||
default_filters: '{}',
|
||||
filter_scopes: {},
|
||||
chart_configuration: {},
|
||||
};
|
||||
|
||||
return cy
|
||||
.request({
|
||||
method: 'PUT',
|
||||
url: `api/v1/dashboard/${dashboardId}`,
|
||||
body: {
|
||||
json_metadata: JSON.stringify(jsonMetadata),
|
||||
},
|
||||
})
|
||||
.then(() => visitDashboard(false));
|
||||
}
|
||||
return cy;
|
||||
});
|
||||
}
|
||||
|
||||
function selectFilter(index: number) {
|
||||
cy.get("[data-test='filter-title-container'] [draggable='true']")
|
||||
.eq(index)
|
||||
@@ -195,219 +68,6 @@ function closeFilterModal() {
|
||||
});
|
||||
}
|
||||
|
||||
function openVerticalFilterBar() {
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
cy.getBySel('filter-bar__expand-button').click();
|
||||
}
|
||||
|
||||
function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') {
|
||||
cy.getBySel('filterbar-orientation-icon').click();
|
||||
cy.wait(250);
|
||||
cy.getBySel('dropdown-selectable-icon-submenu')
|
||||
.contains('Orientation of filter bar')
|
||||
.should('exist')
|
||||
.trigger('mouseover');
|
||||
|
||||
if (orientation === 'vertical') {
|
||||
cy.get('.ant-dropdown-menu-item-selected')
|
||||
.contains('Horizontal (Top)')
|
||||
.should('exist');
|
||||
cy.get('.ant-dropdown-menu-item').contains('Vertical (Left)').click();
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
} else {
|
||||
cy.get('.ant-dropdown-menu-item-selected')
|
||||
.contains('Vertical (Left)')
|
||||
.should('exist');
|
||||
cy.get('.ant-dropdown-menu-item').contains('Horizontal (Top)').click();
|
||||
cy.getBySel('loading-indicator').should('exist');
|
||||
cy.getBySel('filter-bar').should('exist');
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
}
|
||||
}
|
||||
|
||||
function openMoreFilters(intercetFilterState = true) {
|
||||
interceptFilterState();
|
||||
cy.getBySel('dropdown-container-btn').click();
|
||||
|
||||
if (intercetFilterState) {
|
||||
cy.wait('@postFilterState');
|
||||
}
|
||||
}
|
||||
|
||||
describe('Horizontal FilterBar', () => {
|
||||
it('should go from vertical to horizontal and the opposite', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
setFilterBarOrientation('vertical');
|
||||
});
|
||||
|
||||
it('should show all default actions in horizontal mode', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.getBySel('horizontal-filterbar-empty')
|
||||
.contains('No filters are currently added to this dashboard.')
|
||||
.should('exist');
|
||||
cy.getBySel('filter-bar__create-filter').should('exist');
|
||||
cy.getBySel('filterbar-action-buttons').should('exist');
|
||||
});
|
||||
|
||||
it('should stay in horizontal mode when reloading', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.reload();
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show all filters in available space on load', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.get('.filter-item-wrapper').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should show "more filters" on window resizing up and down', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
cy.viewport(768, 1024);
|
||||
cy.getBySel('form-item-value').should('have.length', 0);
|
||||
openMoreFilters(false);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
|
||||
cy.getBySel('filter-bar').click();
|
||||
cy.viewport(1000, 1024);
|
||||
openMoreFilters(false);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
|
||||
cy.getBySel('filter-bar').click();
|
||||
cy.viewport(1300, 1024);
|
||||
cy.getBySel('form-item-value').should('have.length', 3);
|
||||
cy.getBySel('dropdown-container-btn').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show "more filters" and scroll', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.get('.filter-item-wrapper').should('have.length', 3);
|
||||
openMoreFilters();
|
||||
cy.getBySel('form-item-value').should('have.length', 12);
|
||||
cy.getBySel('filter-control-name').contains('test_10').should('be.visible');
|
||||
cy.getBySel('filter-control-name')
|
||||
.contains('test_12')
|
||||
.should('not.be.visible');
|
||||
cy.get('.ant-popover-inner-content').scrollTo('bottom');
|
||||
cy.getBySel('filter-control-name').contains('test_12').should('be.visible');
|
||||
});
|
||||
|
||||
it('should display newly added filter', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
enterNativeFilterEditModal(false);
|
||||
addCountryNameFilter();
|
||||
saveNativeFilterSettings([]);
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it('should spot changes in "more filters" and apply their values', () => {
|
||||
cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
openMoreFilters();
|
||||
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.wait('@chart');
|
||||
cy.get('.antd5-scroll-number.antd5-badge-count').should(
|
||||
'have.attr',
|
||||
'title',
|
||||
'1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should focus filter and open "more filters" programmatically', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
openMoreFilters();
|
||||
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.getBySel('slice-header').within(() => {
|
||||
cy.get('.filter-counts').trigger('mouseover');
|
||||
});
|
||||
cy.get('.filterStatusPopover').contains('test_9').click();
|
||||
cy.getBySel('dropdown-content').should('be.visible');
|
||||
cy.get('.ant-select-focused').should('be.visible');
|
||||
});
|
||||
|
||||
it('should show tag count and one plain tag on focus and only count on blur in select ', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue('Albania');
|
||||
cy.get('.ant-select-selection-search-input').clear({ force: true });
|
||||
inputNativeFilterDefaultValue('Algeria', true);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.getBySel('filter-bar').within(() => {
|
||||
cy.get(nativeFilters.filterItem).contains('Albania').should('be.visible');
|
||||
cy.get(nativeFilters.filterItem).contains('+ 1 ...').should('be.visible');
|
||||
cy.get('.ant-select-selection-search-input').click();
|
||||
cy.get(nativeFilters.filterItem).contains('+ 2 ...').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Native filters', () => {
|
||||
describe('Nativefilters tests initial state required', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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 { SAMPLE_DASHBOARD_1 } from 'cypress/utils/urls';
|
||||
import { interceptCharts, interceptDatasets, interceptGet } from './utils';
|
||||
|
||||
export const SAMPLE_CHART = { name: 'Most Populated Countries', viz: 'table' };
|
||||
|
||||
export function visitDashboard(createSample = true) {
|
||||
interceptCharts();
|
||||
interceptGet();
|
||||
interceptDatasets();
|
||||
|
||||
if (createSample) {
|
||||
cy.createSampleDashboards([0]);
|
||||
}
|
||||
|
||||
cy.visit(SAMPLE_DASHBOARD_1);
|
||||
cy.wait('@get');
|
||||
cy.wait('@getCharts');
|
||||
cy.wait('@getDatasets');
|
||||
cy.url().should('contain', 'native_filters_key');
|
||||
}
|
||||
|
||||
export function prepareDashboardFilters(
|
||||
filters: { name: string; column: string; datasetId: number }[],
|
||||
) {
|
||||
cy.createSampleDashboards([0]);
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `api/v1/dashboard/1-sample-dashboard`,
|
||||
}).then(res => {
|
||||
const { body } = res;
|
||||
const dashboardId = body.result.id;
|
||||
const allFilters: Record<string, unknown>[] = [];
|
||||
filters.forEach((f, i) => {
|
||||
allFilters.push({
|
||||
id: `NATIVE_FILTER-fLH0pxFQ${i}`,
|
||||
controlValues: {
|
||||
enableEmptyFilter: false,
|
||||
defaultToFirstItem: false,
|
||||
multiSelect: true,
|
||||
searchAllOptions: false,
|
||||
inverseSelection: false,
|
||||
},
|
||||
name: f.name,
|
||||
filterType: 'filter_select',
|
||||
targets: [
|
||||
{
|
||||
datasetId: f.datasetId,
|
||||
column: { name: f.column },
|
||||
},
|
||||
],
|
||||
defaultDataMask: {
|
||||
extraFormData: {},
|
||||
filterState: {},
|
||||
ownState: {},
|
||||
},
|
||||
cascadeParentIds: [],
|
||||
scope: {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: [],
|
||||
},
|
||||
type: 'NATIVE_FILTER',
|
||||
description: '',
|
||||
chartsInScope: [5],
|
||||
tabsInScope: [],
|
||||
});
|
||||
});
|
||||
if (dashboardId) {
|
||||
const jsonMetadata = {
|
||||
native_filter_configuration: allFilters,
|
||||
timed_refresh_immune_slices: [],
|
||||
expanded_slices: {},
|
||||
refresh_frequency: 0,
|
||||
color_scheme: '',
|
||||
label_colors: {},
|
||||
shared_label_colors: [],
|
||||
color_scheme_domain: [],
|
||||
cross_filters_enabled: false,
|
||||
positions: {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
|
||||
GRID_ID: {
|
||||
type: 'GRID',
|
||||
id: 'GRID_ID',
|
||||
children: ['ROW-0rHnUz4nMA'],
|
||||
parents: ['ROOT_ID'],
|
||||
},
|
||||
HEADER_ID: {
|
||||
id: 'HEADER_ID',
|
||||
type: 'HEADER',
|
||||
meta: { text: '1 - Sample dashboard' },
|
||||
},
|
||||
'CHART-DF6EfI55F-': {
|
||||
type: 'CHART',
|
||||
id: 'CHART-DF6EfI55F-',
|
||||
children: [],
|
||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-0rHnUz4nMA'],
|
||||
meta: {
|
||||
width: 4,
|
||||
height: 50,
|
||||
chartId: 5,
|
||||
sliceName: 'Most Populated Countries',
|
||||
},
|
||||
},
|
||||
'ROW-0rHnUz4nMA': {
|
||||
type: 'ROW',
|
||||
id: 'ROW-0rHnUz4nMA',
|
||||
children: ['CHART-DF6EfI55F-'],
|
||||
parents: ['ROOT_ID', 'GRID_ID'],
|
||||
meta: { background: 'BACKGROUND_TRANSPARENT' },
|
||||
},
|
||||
},
|
||||
default_filters: '{}',
|
||||
filter_scopes: {},
|
||||
chart_configuration: {},
|
||||
};
|
||||
|
||||
return cy
|
||||
.request({
|
||||
method: 'PUT',
|
||||
url: `api/v1/dashboard/${dashboardId}`,
|
||||
body: {
|
||||
json_metadata: JSON.stringify(jsonMetadata),
|
||||
},
|
||||
})
|
||||
.then(() => visitDashboard(false));
|
||||
}
|
||||
return cy;
|
||||
});
|
||||
}
|
||||
@@ -125,7 +125,7 @@ export const valueNativeFilterOptions = [
|
||||
];
|
||||
|
||||
export function interceptGet() {
|
||||
cy.intercept('/api/v1/dashboard/*').as('get');
|
||||
cy.intercept('GET', '/api/v1/dashboard/*').as('get');
|
||||
}
|
||||
|
||||
export function interceptFiltering() {
|
||||
@@ -144,6 +144,10 @@ export function interceptUpdate() {
|
||||
cy.intercept('PUT', `/api/v1/dashboard/*`).as('update');
|
||||
}
|
||||
|
||||
export function interceptExploreUpdate() {
|
||||
cy.intercept('PUT', `/api/v1/chart/*`).as('chartUpdate');
|
||||
}
|
||||
|
||||
export function interceptPost() {
|
||||
cy.intercept('POST', `/api/v1/dashboard/`).as('post');
|
||||
}
|
||||
@@ -374,7 +378,7 @@ export function cancelNativeFilterSettings() {
|
||||
.should('be.visible')
|
||||
.should('have.text', 'There are unsaved changes.');
|
||||
cy.get(nativeFilters.modal.footer)
|
||||
.find(nativeFilters.modal.yesCancelButton)
|
||||
.find(nativeFilters.modal.confirmCancelButton)
|
||||
.contains('cancel')
|
||||
.click({ force: true });
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
@@ -520,13 +524,17 @@ export function addCountryNameFilter() {
|
||||
);
|
||||
}
|
||||
|
||||
export function openTab(tabComponentIndex: number, tabIndex: number) {
|
||||
return cy
|
||||
.getBySel('dashboard-component-tabs')
|
||||
export function openTab(
|
||||
tabComponentIndex: number,
|
||||
tabIndex: number,
|
||||
target = 'dashboard-component-tabs',
|
||||
) {
|
||||
cy.getBySel(target)
|
||||
.eq(tabComponentIndex)
|
||||
.find('[role="tab"]')
|
||||
.eq(tabIndex)
|
||||
.click();
|
||||
cy.wait(500);
|
||||
}
|
||||
|
||||
export const openTopLevelTab = (tabName: string) => {
|
||||
|
||||
@@ -322,7 +322,9 @@ export const nativeFilters = {
|
||||
footer: '.ant-modal-footer',
|
||||
saveButton: dataTestLocator('native-filter-modal-save-button'),
|
||||
cancelButton: dataTestLocator('native-filter-modal-cancel-button'),
|
||||
yesCancelButton: '[type="button"]',
|
||||
confirmCancelButton: dataTestLocator(
|
||||
'native-filter-modal-confirm-cancel-button',
|
||||
),
|
||||
alertXUnsavedFilters: '.ant-alert-message',
|
||||
tabsList: {
|
||||
filterItemsContainer: dataTestLocator('filter-title-container'),
|
||||
|
||||
363
superset-frontend/package-lock.json
generated
363
superset-frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "superset",
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -67,6 +67,7 @@
|
||||
"core-js": "^3.37.1",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dom-to-image-more": "^3.2.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"emotion-rgba": "0.0.12",
|
||||
"fast-glob": "^3.2.7",
|
||||
"fs-extra": "^10.0.0",
|
||||
@@ -82,6 +83,7 @@
|
||||
"json-bigint": "^1.0.0",
|
||||
"json-stringify-pretty-compact": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.5.0",
|
||||
"mapbox-gl": "^2.10.0",
|
||||
"markdown-to-jsx": "^7.4.7",
|
||||
"match-sorter": "^6.3.4",
|
||||
@@ -92,7 +94,7 @@
|
||||
"mustache": "^2.2.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"polished": "^4.3.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^6.13.7",
|
||||
"rc-trigger": "^5.3.4",
|
||||
"re-resizable": "^6.9.11",
|
||||
@@ -15887,6 +15889,13 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
|
||||
@@ -19582,9 +19591,7 @@
|
||||
},
|
||||
"node_modules/atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"bin": {
|
||||
"atob": "bin/atob.js"
|
||||
},
|
||||
@@ -20474,6 +20481,16 @@
|
||||
"resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
|
||||
"integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ=="
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -20908,6 +20925,18 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/btoa": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"bin": {
|
||||
"btoa": "bin/btoa.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buf-compare": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz",
|
||||
@@ -21467,6 +21496,33 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
|
||||
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/canvg/node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/capture-exit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
|
||||
@@ -23168,6 +23224,16 @@
|
||||
"isobject": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader": {
|
||||
"version": "6.8.1",
|
||||
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz",
|
||||
@@ -25289,11 +25355,26 @@
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-to-image": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "git+ssh://git@github.com/dmapper/dom-to-image.git#a7c386a8ea813930f05449ac71ab4be0c262dff3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-to-image-more": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.2.0.tgz",
|
||||
"integrity": "sha512-2bGQTB6m17MBseVhIjShwZqqqCyVS9GgTykWqvVXMqr56fSgHhXnEvZfZkaSuHJYW3ICZQ3sZwAu+UY5tfsF9Q=="
|
||||
},
|
||||
"node_modules/dom-to-pdf": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-to-pdf/-/dom-to-pdf-0.3.2.tgz",
|
||||
"integrity": "sha512-eHLQ/IK+2PQlRjybQ9UHYwpiTd/YZFKqGFyRCjVvi6CPlH58drWQnxf7HBCVRUyAjOtI3RG0kvLidPhC7dOhcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dom-to-image": "git+https://github.com/dmapper/dom-to-image.git",
|
||||
"jspdf": "^2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-walk": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz",
|
||||
@@ -25324,6 +25405,13 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.5.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.7.tgz",
|
||||
"integrity": "sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||
@@ -28253,6 +28341,12 @@
|
||||
"resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz",
|
||||
"integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag=="
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||
@@ -32171,6 +32265,20 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
@@ -38978,6 +39086,24 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
|
||||
"integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"atob": "^2.1.2",
|
||||
"btoa": "^1.2.1",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.6",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^2.5.4",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jsprim": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
|
||||
@@ -41537,11 +41663,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
|
||||
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -50449,8 +50573,7 @@
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/periscopic": {
|
||||
"version": "3.1.0",
|
||||
@@ -51987,8 +52110,7 @@
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
@@ -55843,6 +55965,16 @@
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
@@ -57394,6 +57526,16 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/static-eval": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.0.tgz",
|
||||
@@ -58021,6 +58163,16 @@
|
||||
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz",
|
||||
@@ -58469,6 +58621,16 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -59899,6 +60061,16 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
@@ -65829,8 +66001,7 @@
|
||||
},
|
||||
"plugins/legacy-preset-chart-nvd3/node_modules/dompurify": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.0.tgz",
|
||||
"integrity": "sha512-yoU4rhgPKCo+p5UrWWWNKiIq+ToGqmVVhk0PmMYBK4kRsR3/qhemNFL8f6CFmBd4gMwm3F4T7HBoydP5uY07fA=="
|
||||
"license": "(MPL-2.0 OR Apache-2.0)"
|
||||
},
|
||||
"plugins/plugin-chart-echarts": {
|
||||
"name": "@superset-ui/plugin-chart-echarts",
|
||||
@@ -78665,9 +78836,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dompurify": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.0.tgz",
|
||||
"integrity": "sha512-yoU4rhgPKCo+p5UrWWWNKiIq+ToGqmVVhk0PmMYBK4kRsR3/qhemNFL8f6CFmBd4gMwm3F4T7HBoydP5uY07fA=="
|
||||
"version": "3.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -80094,6 +80263,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
|
||||
},
|
||||
"@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"optional": true
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
|
||||
@@ -83020,10 +83195,7 @@
|
||||
"dev": true
|
||||
},
|
||||
"atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"dev": true
|
||||
"version": "2.1.2"
|
||||
},
|
||||
"atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
@@ -83723,6 +83895,12 @@
|
||||
"resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
|
||||
"integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ=="
|
||||
},
|
||||
"base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"optional": true
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -84061,6 +84239,11 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"btoa": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g=="
|
||||
},
|
||||
"buf-compare": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz",
|
||||
@@ -84444,6 +84627,30 @@
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz",
|
||||
"integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg=="
|
||||
},
|
||||
"canvg": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
|
||||
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"capture-exit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
|
||||
@@ -85750,6 +85957,15 @@
|
||||
"isobject": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"css-loader": {
|
||||
"version": "6.8.1",
|
||||
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz",
|
||||
@@ -87391,11 +87607,24 @@
|
||||
"entities": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"dom-to-image": {
|
||||
"version": "git+ssh://git@github.com/dmapper/dom-to-image.git#a7c386a8ea813930f05449ac71ab4be0c262dff3",
|
||||
"from": "dom-to-image@git+https://github.com/dmapper/dom-to-image.git"
|
||||
},
|
||||
"dom-to-image-more": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.2.0.tgz",
|
||||
"integrity": "sha512-2bGQTB6m17MBseVhIjShwZqqqCyVS9GgTykWqvVXMqr56fSgHhXnEvZfZkaSuHJYW3ICZQ3sZwAu+UY5tfsF9Q=="
|
||||
},
|
||||
"dom-to-pdf": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-to-pdf/-/dom-to-pdf-0.3.2.tgz",
|
||||
"integrity": "sha512-eHLQ/IK+2PQlRjybQ9UHYwpiTd/YZFKqGFyRCjVvi6CPlH58drWQnxf7HBCVRUyAjOtI3RG0kvLidPhC7dOhcQ==",
|
||||
"requires": {
|
||||
"dom-to-image": "git+https://github.com/dmapper/dom-to-image.git",
|
||||
"jspdf": "^2.5.1"
|
||||
}
|
||||
},
|
||||
"dom-walk": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz",
|
||||
@@ -87414,6 +87643,12 @@
|
||||
"domelementtype": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.5.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.7.tgz",
|
||||
"integrity": "sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q==",
|
||||
"optional": true
|
||||
},
|
||||
"domutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||
@@ -89673,6 +89908,11 @@
|
||||
"resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz",
|
||||
"integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag=="
|
||||
},
|
||||
"fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
|
||||
},
|
||||
"figures": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||
@@ -92558,6 +92798,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
@@ -97525,6 +97775,21 @@
|
||||
"through": ">=2.2.7 <3"
|
||||
}
|
||||
},
|
||||
"jspdf": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
|
||||
"integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"atob": "^2.1.2",
|
||||
"btoa": "^1.2.1",
|
||||
"canvg": "^3.0.6",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^2.5.4",
|
||||
"fflate": "^0.8.1",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
|
||||
@@ -99407,11 +99672,9 @@
|
||||
}
|
||||
},
|
||||
"luxon": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
|
||||
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="
|
||||
},
|
||||
"lz-string": {
|
||||
"version": "1.5.0",
|
||||
@@ -105233,9 +105496,7 @@
|
||||
"dev": true
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
"version": "2.1.0"
|
||||
},
|
||||
"periscopic": {
|
||||
"version": "3.1.0",
|
||||
@@ -106293,8 +106554,6 @@
|
||||
},
|
||||
"raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"requires": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
@@ -109101,6 +109360,12 @@
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"dev": true
|
||||
},
|
||||
"rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"optional": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
@@ -110330,6 +110595,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"optional": true
|
||||
},
|
||||
"static-eval": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.0.tgz",
|
||||
@@ -110793,6 +111064,12 @@
|
||||
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"optional": true
|
||||
},
|
||||
"svgo": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz",
|
||||
@@ -111123,6 +111400,15 @@
|
||||
"integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==",
|
||||
"dev": true
|
||||
},
|
||||
"text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -112169,6 +112455,15 @@
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
|
||||
"dev": true
|
||||
},
|
||||
"utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
|
||||
"keywords": [
|
||||
"big",
|
||||
@@ -132,6 +132,7 @@
|
||||
"core-js": "^3.37.1",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dom-to-image-more": "^3.2.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"emotion-rgba": "0.0.12",
|
||||
"fast-glob": "^3.2.7",
|
||||
"fs-extra": "^10.0.0",
|
||||
@@ -147,6 +148,7 @@
|
||||
"json-bigint": "^1.0.0",
|
||||
"json-stringify-pretty-compact": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.5.0",
|
||||
"mapbox-gl": "^2.10.0",
|
||||
"markdown-to-jsx": "^7.4.7",
|
||||
"match-sorter": "^6.3.4",
|
||||
@@ -157,7 +159,7 @@
|
||||
"mustache": "^2.2.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"polished": "^4.3.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^6.13.7",
|
||||
"rc-trigger": "^5.3.4",
|
||||
"re-resizable": "^6.9.11",
|
||||
|
||||
@@ -55,8 +55,7 @@ const TooltipSection = ({
|
||||
text: ReactNode;
|
||||
}) => (
|
||||
<TooltipSectionWrapper>
|
||||
<TooltipSectionLabel>{label}</TooltipSectionLabel>
|
||||
<span>{text}</span>
|
||||
<TooltipSectionLabel>{label}</TooltipSectionLabel>: <span>{text}</span>
|
||||
</TooltipSectionWrapper>
|
||||
);
|
||||
|
||||
@@ -71,12 +70,7 @@ export const getColumnTypeTooltipNode = (column: ColumnMeta): ReactNode => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipSection
|
||||
label={t('Column datatype')}
|
||||
text={column.type.toLowerCase()}
|
||||
/>
|
||||
);
|
||||
return <TooltipSection label={t('Column type')} text={column.type} />;
|
||||
};
|
||||
|
||||
export const getColumnTooltipNode = (
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
import {
|
||||
formatSelectOptions,
|
||||
displayTimeRelatedControls,
|
||||
getColorControlsProps,
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
@@ -142,9 +143,7 @@ const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
|
||||
renderTrigger: true,
|
||||
schemes: () => sequentialSchemeRegistry.getMap(),
|
||||
isLinear: true,
|
||||
mapStateToProps: state => ({
|
||||
dashboardId: state?.form_data?.dashboardId,
|
||||
}),
|
||||
mapStateToProps: state => getColorControlsProps(state),
|
||||
};
|
||||
|
||||
const granularity: SharedControlConfig<'SelectControl'> = {
|
||||
@@ -333,9 +332,7 @@ const color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
|
||||
choices: () => categoricalSchemeRegistry.keys().map(s => [s, s]),
|
||||
description: t('The color scheme for rendering chart'),
|
||||
schemes: () => categoricalSchemeRegistry.getMap(),
|
||||
mapStateToProps: state => ({
|
||||
dashboardId: state?.form_data?.dashboardId,
|
||||
}),
|
||||
mapStateToProps: state => getColorControlsProps(state),
|
||||
};
|
||||
|
||||
const truncate_metric: SharedControlConfig<'CheckboxControl'> = {
|
||||
|
||||
@@ -87,6 +87,9 @@ export interface Dataset {
|
||||
}
|
||||
|
||||
export interface ControlPanelState {
|
||||
slice: {
|
||||
slice_id: number;
|
||||
};
|
||||
form_data: QueryFormData;
|
||||
datasource: Dataset | QueryResponse | null;
|
||||
controls: ControlStateMapping;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export const getColorControlsProps = (state: Record<string, any>) => {
|
||||
const dashboardId = state?.form_data?.dashboardId;
|
||||
return {
|
||||
chartId: state?.slice?.slice_id,
|
||||
dashboardId,
|
||||
hasDashboardColorScheme:
|
||||
!!dashboardId && !!state?.form_data?.dashboard_color_scheme,
|
||||
hasCustomLabelsColor:
|
||||
Object.keys(state?.form_data?.label_colors || {}).length > 0,
|
||||
colorNamespace: state?.form_data?.color_namespace,
|
||||
mapLabelsColors: state?.form_data?.map_label_colors || {},
|
||||
sharedLabelsColors: state?.form_data?.shared_label_colors || [],
|
||||
};
|
||||
};
|
||||
@@ -27,3 +27,4 @@ export * from './defineSavedMetrics';
|
||||
export * from './getStandardizedControls';
|
||||
export * from './getTemporalColumns';
|
||||
export { default as displayTimeRelatedControls } from './displayTimeRelatedControls';
|
||||
export * from './colorControls';
|
||||
|
||||
@@ -90,7 +90,7 @@ test('should get column datatype rendered as tooltip when column has a type', ()
|
||||
</>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Column datatype')).toBeVisible();
|
||||
expect(screen.getByText('Column type')).toBeVisible();
|
||||
expect(screen.getByText('text')).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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 { getColorControlsProps } from '../../src';
|
||||
|
||||
describe('getColorControlsProps', () => {
|
||||
it('should return default values when state is empty', () => {
|
||||
const state = {};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: undefined,
|
||||
dashboardId: undefined,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: false,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct values when state has form_data with dashboardId and color scheme', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 123,
|
||||
dashboard_color_scheme: 'blueScheme',
|
||||
label_colors: {},
|
||||
},
|
||||
slice: { slice_id: 456 },
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: 456,
|
||||
dashboardId: 123,
|
||||
hasDashboardColorScheme: true,
|
||||
hasCustomLabelsColor: false,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect custom label colors correctly', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 123,
|
||||
label_colors: { label1: '#000000' },
|
||||
},
|
||||
slice: { slice_id: 456 },
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: 456,
|
||||
dashboardId: 123,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: true,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return shared label colors when available', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
shared_label_colors: ['#FF5733', '#33FF57'],
|
||||
},
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: undefined,
|
||||
dashboardId: undefined,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: false,
|
||||
sharedLabelsColors: ['#FF5733', '#33FF57'],
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing form_data and slice properties', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 789,
|
||||
},
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: undefined,
|
||||
dashboardId: 789,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: false,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import CategoricalColorScale from './CategoricalColorScale';
|
||||
import { ColorsLookup } from './types';
|
||||
import getCategoricalSchemeRegistry from './CategoricalSchemeRegistrySingleton';
|
||||
@@ -37,10 +38,21 @@ export default class CategoricalColorNamespace {
|
||||
this.forcedItems = {};
|
||||
}
|
||||
|
||||
getScale(schemeId?: string) {
|
||||
const id = schemeId ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
|
||||
/**
|
||||
* A new CategoricalColorScale instance is created for each chart.
|
||||
*
|
||||
* @param colorScheme - the color scheme to use
|
||||
* @returns a new instance of a color scale
|
||||
*/
|
||||
getScale(colorScheme?: string) {
|
||||
const id =
|
||||
colorScheme ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
|
||||
const scheme = getCategoricalSchemeRegistry().get(id);
|
||||
return new CategoricalColorScale(scheme?.colors ?? [], this.forcedItems);
|
||||
return new CategoricalColorScale(
|
||||
scheme?.colors ?? [],
|
||||
this.forcedItems,
|
||||
colorScheme,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,6 +71,17 @@ export default class CategoricalColorNamespace {
|
||||
resetColors() {
|
||||
this.forcedItems = {};
|
||||
}
|
||||
|
||||
resetColorsForLabels(labels: string[] = []) {
|
||||
const updatedForcedItems = cloneDeep(this.forcedItems);
|
||||
labels.forEach(label => {
|
||||
if (updatedForcedItems.hasOwnProperty(label)) {
|
||||
delete updatedForcedItems[label];
|
||||
}
|
||||
});
|
||||
|
||||
this.forcedItems = { ...updatedForcedItems };
|
||||
}
|
||||
}
|
||||
|
||||
const namespaces: {
|
||||
@@ -80,16 +103,19 @@ export function getNamespace(name: string = DEFAULT_NAMESPACE) {
|
||||
|
||||
export function getColor(
|
||||
value?: string,
|
||||
schemeId?: string,
|
||||
colorScheme?: string,
|
||||
namespace?: string,
|
||||
) {
|
||||
return getNamespace(namespace).getScale(schemeId).getColor(value);
|
||||
return getNamespace(namespace).getScale(colorScheme).getColor(value);
|
||||
}
|
||||
|
||||
/*
|
||||
Returns a new scale instance within the same namespace.
|
||||
Especially useful when a chart is booting for the first time
|
||||
|
||||
@param scheme - the applied color scheme
|
||||
@param namespace - the namespace
|
||||
*/
|
||||
export function getScale(scheme?: string, namespace?: string) {
|
||||
return getNamespace(namespace).getScale(scheme);
|
||||
export function getScale(colorScheme?: string, namespace?: string) {
|
||||
return getNamespace(namespace).getScale(colorScheme);
|
||||
}
|
||||
|
||||
@@ -21,14 +21,16 @@ import { scaleOrdinal, ScaleOrdinal } from 'd3-scale';
|
||||
import { ExtensibleFunction } from '../models';
|
||||
import { ColorsInitLookup, ColorsLookup } from './types';
|
||||
import stringifyAndTrim from './stringifyAndTrim';
|
||||
import getLabelsColorMap from './LabelsColorMapSingleton';
|
||||
import getLabelsColorMap, {
|
||||
LabelsColorMapSource,
|
||||
} from './LabelsColorMapSingleton';
|
||||
import { getAnalogousColors } from './utils';
|
||||
import { FeatureFlag, isFeatureEnabled } from '../utils';
|
||||
|
||||
// Use type augmentation to correct the fact that
|
||||
// an instance of CategoricalScale is also a function
|
||||
interface CategoricalColorScale {
|
||||
(x: { toString(): string }, y?: number, w?: string): string;
|
||||
(x: { toString(): string }, y?: number): string;
|
||||
}
|
||||
|
||||
class CategoricalColorScale extends ExtensibleFunction {
|
||||
@@ -50,11 +52,16 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
* Constructor
|
||||
* @param {*} colors an array of colors
|
||||
* @param {*} forcedColors optional parameter that comes from parent
|
||||
* (usually CategoricalColorNamespace)
|
||||
* @param {*} appliedColorScheme the color scheme applied to the chart
|
||||
*
|
||||
*/
|
||||
constructor(colors: string[], forcedColors: ColorsInitLookup = {}) {
|
||||
super((value: string, sliceId?: number, colorScheme?: string) =>
|
||||
this.getColor(value, sliceId, colorScheme),
|
||||
constructor(
|
||||
colors: string[],
|
||||
forcedColors: ColorsInitLookup = {},
|
||||
appliedColorScheme?: string,
|
||||
) {
|
||||
super((value: string, sliceId?: number) =>
|
||||
this.getColor(value, sliceId, appliedColorScheme),
|
||||
);
|
||||
// holds original color scheme colors
|
||||
this.originColors = colors;
|
||||
@@ -107,15 +114,28 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
*
|
||||
* @param value the value of a label to get the color for
|
||||
* @param sliceId the ID of the current chart
|
||||
* @param colorScheme the original color scheme of the chart
|
||||
* @param appliedColorScheme the color scheme applied to the chart
|
||||
* @returns the color or the next available color
|
||||
*/
|
||||
getColor(value?: string, sliceId?: number, colorScheme?: string): string {
|
||||
getColor(
|
||||
value?: string,
|
||||
sliceId?: number,
|
||||
appliedColorScheme?: string,
|
||||
): string {
|
||||
const cleanedValue = stringifyAndTrim(value);
|
||||
// priority: forced color (i.e. custom label colors) > shared color > scale color
|
||||
// priority: forced color (aka custom label colors) > shared color > scale color
|
||||
const forcedColor = this.forcedColors?.[cleanedValue];
|
||||
const isExistingLabel = this.chartLabelsColorMap.has(cleanedValue);
|
||||
let color = forcedColor || this.scale(cleanedValue);
|
||||
const { source } = this.labelsColorMapInstance;
|
||||
const currentColorMap =
|
||||
source === LabelsColorMapSource.Dashboard
|
||||
? this.labelsColorMapInstance.getColorMap()
|
||||
: this.chartLabelsColorMap;
|
||||
const isExistingLabel = currentColorMap.has(cleanedValue);
|
||||
let color =
|
||||
forcedColor ||
|
||||
(isExistingLabel
|
||||
? (currentColorMap.get(cleanedValue) as string)
|
||||
: this.scale(cleanedValue));
|
||||
|
||||
// a forced color will always be used independently of the usage count
|
||||
if (!forcedColor && !isExistingLabel) {
|
||||
@@ -128,7 +148,7 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
this.isColorUsed(color)
|
||||
) {
|
||||
// fallback to least used color
|
||||
color = this.getNextAvailableColor(color);
|
||||
color = this.getNextAvailableColor(cleanedValue, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +161,7 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
cleanedValue,
|
||||
color,
|
||||
sliceId,
|
||||
colorScheme,
|
||||
appliedColorScheme,
|
||||
);
|
||||
}
|
||||
return color;
|
||||
@@ -164,48 +184,76 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
* @param color the color to check
|
||||
* @returns the count of the color usage in this slice
|
||||
*/
|
||||
getColorUsageCount(currentColor: string): number {
|
||||
let count = 0;
|
||||
this.chartLabelsColorMap.forEach(color => {
|
||||
if (color === currentColor) {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
getColorUsageCount(color: string): number {
|
||||
return Array.from(this.chartLabelsColorMap.values()).filter(
|
||||
value => value === color,
|
||||
).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lower chances of color collision by returning the least used color
|
||||
* Checks across colors of current slice within LabelsColorMapSingleton
|
||||
* Lower chances of color collision by returning the least used color.
|
||||
* Checks across colors of current slice within chartLabelsColorMap.
|
||||
*
|
||||
* @param currentLabel the current label
|
||||
* @param currentColor the current color
|
||||
* @returns the least used color that is not the excluded color
|
||||
* @returns the least used color that is not the current color
|
||||
*/
|
||||
getNextAvailableColor(currentColor: string) {
|
||||
const colorUsageArray = this.colors.map(color => ({
|
||||
color,
|
||||
count: this.getColorUsageCount(color),
|
||||
}));
|
||||
const currentColorCount = this.getColorUsageCount(currentColor);
|
||||
const otherColors = colorUsageArray.filter(
|
||||
colorEntry => colorEntry.color !== currentColor,
|
||||
);
|
||||
// all other colors are used as much or more than currentColor
|
||||
const hasNoneAvailable = otherColors.every(
|
||||
colorEntry => colorEntry.count >= currentColorCount,
|
||||
getNextAvailableColor(currentLabel: string, currentColor: string): string {
|
||||
// Precompute color usage counts for all colors
|
||||
const colorUsageCounts = new Map(
|
||||
this.colors.map(color => [color, this.getColorUsageCount(color)]),
|
||||
);
|
||||
|
||||
// fallback to currentColor color
|
||||
if (!otherColors.length || hasNoneAvailable) {
|
||||
return currentColor;
|
||||
// Get an ordered array of labels from the map
|
||||
const orderedLabels = Array.from(this.chartLabelsColorMap.keys());
|
||||
const currentLabelIndex = orderedLabels.indexOf(currentLabel);
|
||||
|
||||
// Helper to infer "previous" and "next" labels based on index
|
||||
const getAdjacentLabelsColors = (): string[] => {
|
||||
const previousLabel =
|
||||
currentLabelIndex > 0 ? orderedLabels[currentLabelIndex - 1] : null;
|
||||
const nextLabel =
|
||||
currentLabelIndex < orderedLabels.length - 1
|
||||
? orderedLabels[currentLabelIndex + 1]
|
||||
: null;
|
||||
|
||||
const previousColor = previousLabel
|
||||
? this.chartLabelsColorMap.get(previousLabel)
|
||||
: null;
|
||||
const nextColor = nextLabel
|
||||
? this.chartLabelsColorMap.get(nextLabel)
|
||||
: null;
|
||||
|
||||
return [previousColor, nextColor].filter(color => color) as string[];
|
||||
};
|
||||
|
||||
const adjacentColors = getAdjacentLabelsColors();
|
||||
|
||||
// Determine adjusted score (usage count + penalties)
|
||||
const calculateScore = (color: string): number => {
|
||||
/* istanbul ignore next */
|
||||
const usageCount = colorUsageCounts.get(color) || 0;
|
||||
const adjacencyPenalty = adjacentColors.includes(color) ? 100 : 0;
|
||||
return usageCount + adjacencyPenalty;
|
||||
};
|
||||
|
||||
// If there is any color that has never been used, prioritize it
|
||||
const unusedColor = this.colors.find(
|
||||
color => (colorUsageCounts.get(color) || 0) === 0,
|
||||
);
|
||||
if (unusedColor) {
|
||||
return unusedColor;
|
||||
}
|
||||
|
||||
// Finding the least used color
|
||||
const leastUsedColor = otherColors.reduce((min, entry) =>
|
||||
entry.count < min.count ? entry : min,
|
||||
).color;
|
||||
// If all colors are used, calculate scores and choose the best one
|
||||
const otherColors = this.colors.filter(color => color !== currentColor);
|
||||
|
||||
return leastUsedColor;
|
||||
// Find the color with the minimum score, defaulting to currentColor
|
||||
return otherColors.reduce((bestColor, color) => {
|
||||
const bestScore = calculateScore(bestColor);
|
||||
const currentScore = calculateScore(color);
|
||||
return currentScore < bestScore ? color : bestColor;
|
||||
}, currentColor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import { makeSingleton } from '../utils';
|
||||
import CategoricalColorNamespace from './CategoricalColorNamespace';
|
||||
|
||||
export enum LabelsColorMapSource {
|
||||
Dashboard,
|
||||
@@ -25,7 +26,10 @@ export enum LabelsColorMapSource {
|
||||
}
|
||||
|
||||
export class LabelsColorMap {
|
||||
chartsLabelsMap: Map<number, { labels: string[]; scheme?: string }>;
|
||||
chartsLabelsMap: Map<
|
||||
number,
|
||||
{ labels: string[]; scheme?: string; ownScheme?: string }
|
||||
>;
|
||||
|
||||
colorMap: Map<string, string>;
|
||||
|
||||
@@ -38,17 +42,38 @@ export class LabelsColorMap {
|
||||
this.source = LabelsColorMapSource.Dashboard;
|
||||
}
|
||||
|
||||
updateColorMap(categoricalNamespace: any, colorScheme?: string) {
|
||||
const newColorMap = new Map();
|
||||
this.colorMap.clear();
|
||||
/**
|
||||
* Wipes out the color map and updates it with the new color scheme.
|
||||
*
|
||||
* @param categoricalNamespace - the namespace to use for color mapping
|
||||
* @param colorScheme - color scheme
|
||||
*/
|
||||
updateColorMap(
|
||||
categoricalNamespace: CategoricalColorNamespace,
|
||||
colorScheme?: string,
|
||||
merge = false,
|
||||
) {
|
||||
const newColorMap = this.colorMap;
|
||||
|
||||
if (!merge) {
|
||||
newColorMap.clear();
|
||||
}
|
||||
|
||||
this.chartsLabelsMap.forEach((chartConfig, sliceId) => {
|
||||
const { labels, scheme: originalChartColorScheme } = chartConfig;
|
||||
const currentColorScheme = colorScheme || originalChartColorScheme;
|
||||
const colorScale = categoricalNamespace.getScale(currentColorScheme);
|
||||
const { labels, ownScheme } = chartConfig;
|
||||
const appliedColorScheme = colorScheme || ownScheme;
|
||||
const colorScale = categoricalNamespace.getScale(appliedColorScheme);
|
||||
|
||||
labels.forEach(label => {
|
||||
const newColor = colorScale.getColor(label, sliceId);
|
||||
newColorMap.set(label, newColor);
|
||||
// if merge, apply the scheme only to new labels in the map
|
||||
if (!merge || !this.colorMap.has(label)) {
|
||||
const newColor = colorScale.getColor(
|
||||
label,
|
||||
sliceId,
|
||||
appliedColorScheme,
|
||||
);
|
||||
newColorMap.set(label, newColor);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.colorMap = newColorMap;
|
||||
@@ -58,29 +83,63 @@ export class LabelsColorMap {
|
||||
return this.colorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Called individually by each plugin via getColor fn.
|
||||
*
|
||||
* @param label - the label name
|
||||
* @param color - the color
|
||||
* @param sliceId - the chart id
|
||||
* @param colorScheme - the color scheme
|
||||
*
|
||||
*/
|
||||
addSlice(
|
||||
label: string,
|
||||
color: string,
|
||||
sliceId: number,
|
||||
colorScheme?: string,
|
||||
) {
|
||||
if (this.source !== LabelsColorMapSource.Dashboard) return;
|
||||
|
||||
const chartConfig = this.chartsLabelsMap.get(sliceId) || {
|
||||
labels: [],
|
||||
scheme: '',
|
||||
scheme: undefined,
|
||||
ownScheme: undefined,
|
||||
};
|
||||
|
||||
const { labels } = chartConfig;
|
||||
if (!labels.includes(label)) {
|
||||
labels.push(label);
|
||||
this.chartsLabelsMap.set(sliceId, {
|
||||
labels,
|
||||
scheme: colorScheme,
|
||||
ownScheme: chartConfig.ownScheme,
|
||||
});
|
||||
}
|
||||
this.colorMap.set(label, color);
|
||||
if (this.source === LabelsColorMapSource.Dashboard) {
|
||||
this.colorMap.set(label, color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to make sure all slices respect their original scheme.
|
||||
*
|
||||
* @param sliceId - the chart id
|
||||
* @param ownScheme - the color scheme
|
||||
*/
|
||||
setOwnColorScheme(sliceId: number, ownScheme: string) {
|
||||
const chartConfig = this.chartsLabelsMap.get(sliceId);
|
||||
if (chartConfig) {
|
||||
this.chartsLabelsMap.set(sliceId, {
|
||||
...chartConfig,
|
||||
ownScheme,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a slice from the color map.
|
||||
*
|
||||
* @param sliceId - the chart
|
||||
*/
|
||||
removeSlice(sliceId: number) {
|
||||
if (this.source !== LabelsColorMapSource.Dashboard) return;
|
||||
|
||||
@@ -96,10 +155,20 @@ export class LabelsColorMap {
|
||||
this.colorMap = newColorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the shared labels color map.
|
||||
*/
|
||||
clear() {
|
||||
this.chartsLabelsMap.clear();
|
||||
this.colorMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all maps
|
||||
*/
|
||||
reset() {
|
||||
this.clear();
|
||||
this.chartsLabelsMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const getInstance = makeSingleton(LabelsColorMap);
|
||||
|
||||
@@ -60,6 +60,8 @@ export enum FeatureFlag {
|
||||
UseAnalagousColors = 'USE_ANALAGOUS_COLORS',
|
||||
ForceSqlLabRunAsync = 'SQLLAB_FORCE_RUN_ASYNC',
|
||||
SlackEnableAvatars = 'SLACK_ENABLE_AVATARS',
|
||||
EnableDashboardScreenshotEndpoints = 'ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS',
|
||||
EnableDashboardDownloadWebDriverScreenshot = 'ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT',
|
||||
}
|
||||
|
||||
export type ScheduleQueriesProps = {
|
||||
|
||||
@@ -161,4 +161,63 @@ describe('CategoricalColorNamespace', () => {
|
||||
expect(color).toBe(color2);
|
||||
});
|
||||
});
|
||||
describe('statis resetColorsForLabels(labels)', () => {
|
||||
it('removes specified labels from forcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.setColor('label2', 'blue');
|
||||
namespace.resetColorsForLabels(['label1']);
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({ label2: 'blue' });
|
||||
});
|
||||
it('does not modify forcedItems if no labels are provided', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.setColor('label2', 'blue');
|
||||
namespace.resetColorsForLabels();
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({
|
||||
label1: 'red',
|
||||
label2: 'blue',
|
||||
});
|
||||
});
|
||||
it('does nothing if the label is not in forcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.resetColorsForLabels(['label2']); // label2 doesn't exist
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({ label1: 'red' });
|
||||
});
|
||||
it('removes all labels when all are provided', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.setColor('label2', 'blue');
|
||||
namespace.resetColorsForLabels(['label1', 'label2']);
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({});
|
||||
});
|
||||
it('creates a deep copy of forcedItems before modifying', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
|
||||
const originalForcedItems = namespace.forcedItems;
|
||||
namespace.resetColorsForLabels(['label1']);
|
||||
|
||||
expect(originalForcedItems).not.toBe(namespace.forcedItems);
|
||||
});
|
||||
it('removes the label if it exists in updatedForcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.resetColorsForLabels(['label1']);
|
||||
|
||||
expect(namespace.forcedItems).toEqual({});
|
||||
});
|
||||
it('does nothing for a label not in updatedForcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.resetColorsForLabels(['label2']); // label2 doesn't exist
|
||||
|
||||
expect(namespace.forcedItems).toEqual({ label1: 'red' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
*/
|
||||
|
||||
import { ScaleOrdinal } from 'd3-scale';
|
||||
import { CategoricalColorScale, FeatureFlag } from '@superset-ui/core';
|
||||
import {
|
||||
CategoricalColorScale,
|
||||
FeatureFlag,
|
||||
LabelsColorMapSource,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
describe('CategoricalColorScale', () => {
|
||||
beforeEach(() => {
|
||||
@@ -43,7 +47,6 @@ describe('CategoricalColorScale', () => {
|
||||
expect(scale).toBeInstanceOf(CategoricalColorScale);
|
||||
expect(scale.forcedColors).toBe(forcedColors);
|
||||
});
|
||||
|
||||
it('can refer to colors based on their index', () => {
|
||||
const forcedColors = { pig: 1, horse: 5 };
|
||||
const scale = new CategoricalColorScale(
|
||||
@@ -67,7 +70,7 @@ describe('CategoricalColorScale', () => {
|
||||
>;
|
||||
let getNextAvailableColorSpy: jest.SpyInstance<
|
||||
string,
|
||||
[currentColor: string]
|
||||
[currentLabel: string, currentColor: string]
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -83,6 +86,36 @@ describe('CategoricalColorScale', () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('uses labelsColorMapInstance color map when source is Dashboard, otherwise uses chartLabelsColorMap', () => {
|
||||
const sliceId = 123;
|
||||
const colorScheme = 'preset';
|
||||
|
||||
// Mock chartLabelsColorMap and labelsColorMapInstance's getColorMap
|
||||
const chartColorMap = new Map([['testValueChart', 'chartColor']]);
|
||||
const dashboardColorMap = new Map([['testValueDash', 'dashboardColor']]);
|
||||
scale.chartLabelsColorMap = chartColorMap;
|
||||
jest
|
||||
.spyOn(scale.labelsColorMapInstance, 'getColorMap')
|
||||
.mockReturnValue(dashboardColorMap);
|
||||
|
||||
// Test when source is Dashboard
|
||||
scale.labelsColorMapInstance.source = LabelsColorMapSource.Dashboard;
|
||||
const colorFromDashboard = scale.getColor(
|
||||
'testValueDash',
|
||||
sliceId,
|
||||
colorScheme,
|
||||
);
|
||||
expect(colorFromDashboard).toBe('dashboardColor');
|
||||
|
||||
// Test when source is not Dashboard
|
||||
scale.labelsColorMapInstance.source = LabelsColorMapSource.Explore;
|
||||
const colorFromChart = scale.getColor(
|
||||
'testValueChart',
|
||||
sliceId,
|
||||
colorScheme,
|
||||
);
|
||||
expect(colorFromChart).toBe('chartColor');
|
||||
});
|
||||
it('returns same color for same value', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green'], {
|
||||
pig: 'red',
|
||||
@@ -177,7 +210,10 @@ describe('CategoricalColorScale', () => {
|
||||
scale.getColor('testValue3');
|
||||
scale.getColor('testValue4');
|
||||
|
||||
expect(getNextAvailableColorSpy).toHaveBeenCalledWith('blue');
|
||||
expect(getNextAvailableColorSpy).toHaveBeenCalledWith(
|
||||
'testValue4',
|
||||
'blue',
|
||||
);
|
||||
|
||||
getNextAvailableColorSpy.mockClear();
|
||||
|
||||
@@ -289,23 +325,25 @@ describe('CategoricalColorScale', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getNextAvailableColor(currentColor)', () => {
|
||||
describe('.getNextAvailableColor(currentLabel, currentColor)', () => {
|
||||
it('returns the current color if it is the least used or equally used among colors', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat');
|
||||
scale.getColor('dog');
|
||||
|
||||
// Since 'green' hasn't been used, it's considered the least used.
|
||||
expect(scale.getNextAvailableColor('blue')).toBe('green');
|
||||
expect(scale.getNextAvailableColor('fish', 'blue')).toBe('green');
|
||||
});
|
||||
|
||||
it('handles cases where all colors are equally used and returns the current color', () => {
|
||||
it('returns the least used color among all', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
scale.getColor('fish'); // green
|
||||
// All colors used once, so the function should return the current color
|
||||
expect(scale.getNextAvailableColor('red')).toBe('red');
|
||||
scale.getColor('puppy'); // blue
|
||||
scale.getColor('teddy'); // red
|
||||
// All colors used, so the function should return least used
|
||||
expect(scale.getNextAvailableColor('darling', 'red')).toBe('green');
|
||||
});
|
||||
|
||||
it('returns the least used color accurately even when some colors are used more frequently', () => {
|
||||
@@ -324,7 +362,57 @@ describe('CategoricalColorScale', () => {
|
||||
scale.getColor('pony'); // green
|
||||
|
||||
// Yellow is the least used color, so it should be returned.
|
||||
expect(scale.getNextAvailableColor('blue')).toBe('yellow');
|
||||
expect(scale.getNextAvailableColor('pony', 'blue')).toBe('yellow');
|
||||
});
|
||||
it('does not return adjacent colors if a non-adjacent color is equally used', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.chartLabelsColorMap.set('label1', 'red'); // Adjacent
|
||||
scale.chartLabelsColorMap.set('label2', 'blue'); // currentLabel
|
||||
scale.chartLabelsColorMap.set('label3', 'green'); // Adjacent
|
||||
|
||||
// Green and blue are equally used, but green is adjacent and penalized.
|
||||
expect(scale.getNextAvailableColor('label2', 'blue')).toBe('blue');
|
||||
});
|
||||
it('prioritizes a color that has never been used, even if there are adjacent colors', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
|
||||
scale.chartLabelsColorMap.set('label1', 'red');
|
||||
scale.chartLabelsColorMap.set('label2', 'blue'); // currentLabel
|
||||
|
||||
// Green has never been used, so it is prioritized.
|
||||
expect(scale.getNextAvailableColor('label2', 'blue')).toBe('green');
|
||||
});
|
||||
it('returns the least used or unused color when there are no adjacent labels', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
|
||||
// No adjacent labels are defined in chartLabelsColorMap.
|
||||
expect(scale.getNextAvailableColor('label2', 'green')).toBe('green');
|
||||
});
|
||||
it('handles colors that have never been used (fallback to usage count 0)', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
|
||||
// Do not use "green" at all
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
|
||||
// "green" has never been used, so usageCount for "green" should fallback to 0
|
||||
expect(scale.getNextAvailableColor('label2', 'red')).toBe('green');
|
||||
});
|
||||
it('handles a color with an explicit usage count of 0', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
|
||||
// Mock or override getColorUsageCount to return 0 for "blue"
|
||||
jest.spyOn(scale, 'getColorUsageCount').mockImplementation(color => {
|
||||
if (color === 'blue') return 0; // Explicitly return 0 for "blue"
|
||||
return 1; // Return 1 for other colors
|
||||
});
|
||||
|
||||
// "blue" should still be a valid option with a usage count of 0
|
||||
expect(scale.getNextAvailableColor('label1', 'red')).toBe('blue');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('LabelsColorMap', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
getLabelsColorMap().source = LabelsColorMapSource.Dashboard;
|
||||
getLabelsColorMap().clear();
|
||||
getLabelsColorMap().reset();
|
||||
});
|
||||
|
||||
it('has default value out-of-the-box', () => {
|
||||
@@ -92,11 +92,17 @@ describe('LabelsColorMap', () => {
|
||||
expect(Object.fromEntries(colorMap)).toEqual({ b: 'green' });
|
||||
});
|
||||
|
||||
it('should do nothing when source is not dashboard', () => {
|
||||
it('should set a new color only when source is dashboard', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Explore;
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
expect(Object.fromEntries(labelsColorMap.chartsLabelsMap)).toEqual({});
|
||||
const colorMap = labelsColorMap.getColorMap();
|
||||
expect(Object.fromEntries(colorMap)).toEqual({});
|
||||
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
const colorMap2 = labelsColorMap.getColorMap();
|
||||
expect(Object.fromEntries(colorMap2)).toEqual({ a: 'red' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,7 +132,7 @@ describe('LabelsColorMap', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.updateColorMap(namespace, scheme)', () => {
|
||||
describe('.updateColorMap(namespace, scheme, merge)', () => {
|
||||
let categoricalNamespace: any;
|
||||
let mockedNamespace: any;
|
||||
let labelsColorMap: any;
|
||||
@@ -141,18 +147,24 @@ describe('LabelsColorMap', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('should clear color map when not merge', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2', false);
|
||||
expect(labelsColorMap.colorMap).toEqual(new Map([['a', 'mockColor']]));
|
||||
});
|
||||
|
||||
it('should not clear color map when merge', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2', true);
|
||||
expect(labelsColorMap.colorMap).not.toEqual(new Map());
|
||||
});
|
||||
|
||||
it('should use provided color scheme', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2');
|
||||
expect(mockedNamespace.getScale).toHaveBeenCalledWith('testColors2');
|
||||
});
|
||||
|
||||
it('should fallback to original chart color scheme if no color scheme is provided', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1, 'originalScheme');
|
||||
labelsColorMap.updateColorMap(mockedNamespace);
|
||||
expect(mockedNamespace.getScale).toHaveBeenCalledWith('originalScheme');
|
||||
});
|
||||
|
||||
it('should fallback to undefined if no color scheme is provided', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.addSlice('b', 'blue', 2);
|
||||
@@ -181,6 +193,23 @@ describe('LabelsColorMap', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should update only new labels in the color map when merge', () => {
|
||||
labelsColorMap.colorMap = new Map();
|
||||
|
||||
labelsColorMap.addSlice('a', 'yellow', 1);
|
||||
labelsColorMap.addSlice('b', 'green', 1);
|
||||
labelsColorMap.addSlice('c', 'purple', 1);
|
||||
|
||||
labelsColorMap.updateColorMap(categoricalNamespace, 'testColors2', true);
|
||||
|
||||
const mergedColorMap = labelsColorMap.getColorMap();
|
||||
expect(Object.fromEntries(mergedColorMap)).toEqual({
|
||||
a: 'yellow',
|
||||
b: 'green',
|
||||
c: 'purple',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use recycle colors', () => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.UseAnalagousColors]: false,
|
||||
@@ -231,4 +260,47 @@ describe('LabelsColorMap', () => {
|
||||
expect(Object.fromEntries(colorMap)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOwnColorScheme(sliceId, ownScheme)', () => {
|
||||
it('should update the scheme in the config', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Explore;
|
||||
const sliceId = 1;
|
||||
const initialConfig = { labels: ['initial config'] };
|
||||
|
||||
labelsColorMap.chartsLabelsMap = new Map();
|
||||
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
|
||||
|
||||
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
|
||||
|
||||
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
|
||||
...initialConfig,
|
||||
ownScheme: 'newScheme',
|
||||
});
|
||||
});
|
||||
it('should update ownScheme when source is not Explore', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
const sliceId = 1;
|
||||
const initialConfig = { labels: ['initial config'] };
|
||||
|
||||
labelsColorMap.chartsLabelsMap = new Map();
|
||||
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
|
||||
|
||||
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
|
||||
|
||||
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
|
||||
...initialConfig,
|
||||
ownScheme: 'newScheme',
|
||||
});
|
||||
});
|
||||
it('should do nothing when chart config does not exist', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Explore;
|
||||
const sliceId = 1;
|
||||
|
||||
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
|
||||
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ function Chord(element, props) {
|
||||
.append('path')
|
||||
.attr('id', (d, i) => `group${i}`)
|
||||
.attr('d', arc)
|
||||
.style('fill', (d, i) => colorFn(nodes[i], sliceId, colorScheme));
|
||||
.style('fill', (d, i) => colorFn(nodes[i], sliceId));
|
||||
|
||||
// Add a text label.
|
||||
const groupText = group.append('text').attr('x', 6).attr('dy', 15);
|
||||
@@ -121,7 +121,7 @@ function Chord(element, props) {
|
||||
.on('mouseover', d => {
|
||||
chord.classed('fade', p => p !== d);
|
||||
})
|
||||
.style('fill', d => colorFn(nodes[d.source.index], sliceId, colorScheme))
|
||||
.style('fill', d => colorFn(nodes[d.source.index], sliceId))
|
||||
.attr('d', path);
|
||||
|
||||
// Add an elaborate mouseover title for each chord.
|
||||
|
||||
@@ -37,6 +37,7 @@ const propTypes = {
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
country: PropTypes.string,
|
||||
colorScheme: PropTypes.string,
|
||||
linearColorScheme: PropTypes.string,
|
||||
mapBaseUrl: PropTypes.string,
|
||||
numberFormat: PropTypes.string,
|
||||
|
||||
@@ -73,12 +73,11 @@ class CustomHistogram extends PureComponent {
|
||||
showLegend,
|
||||
sliceId,
|
||||
} = this.props;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const keys = data.map(d => d.key);
|
||||
const colorScale = scaleOrdinal({
|
||||
domain: keys,
|
||||
range: keys.map(x => colorFn(x, sliceId, colorScheme)),
|
||||
range: keys.map(x => colorFn(x, sliceId)),
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -384,7 +384,7 @@ function Icicle(element, props) {
|
||||
|
||||
// Apply color scheme
|
||||
g.selectAll('rect').style('fill', d => {
|
||||
d.color = colorFn(d.name, sliceId, colorScheme);
|
||||
d.color = colorFn(d.name, sliceId);
|
||||
|
||||
return d.color;
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ const propTypes = {
|
||||
numberFormat: PropTypes.string,
|
||||
useRichTooltip: PropTypes.bool,
|
||||
useAreaProportions: PropTypes.bool,
|
||||
colorScheme: PropTypes.string,
|
||||
};
|
||||
|
||||
function copyArc(d) {
|
||||
@@ -120,14 +121,14 @@ function Rose(element, props) {
|
||||
.map(v => ({
|
||||
key: v.name,
|
||||
value: v.value,
|
||||
color: colorFn(v.name, sliceId, colorScheme),
|
||||
color: colorFn(v.name, sliceId),
|
||||
highlight: v.id === d.arcId,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: d.name,
|
||||
value: d.val,
|
||||
color: colorFn(d.name, sliceId, colorScheme),
|
||||
color: colorFn(d.name, sliceId),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -138,7 +139,7 @@ function Rose(element, props) {
|
||||
};
|
||||
}
|
||||
|
||||
legend.width(width).color(d => colorFn(d.key, sliceId, colorScheme));
|
||||
legend.width(width).color(d => colorFn(d.key, sliceId));
|
||||
legendWrap.datum(legendData(datum)).call(legend);
|
||||
|
||||
tooltip.headerFormatter(timeFormat).valueFormatter(format);
|
||||
@@ -385,7 +386,7 @@ function Rose(element, props) {
|
||||
const arcs = ae
|
||||
.append('path')
|
||||
.attr('class', 'arc')
|
||||
.attr('fill', d => colorFn(d.name, sliceId, colorScheme))
|
||||
.attr('fill', d => colorFn(d.name, sliceId))
|
||||
.attr('d', arc);
|
||||
|
||||
function mousemove() {
|
||||
|
||||
@@ -83,7 +83,7 @@ function computeGraph(links) {
|
||||
|
||||
function SankeyLoop(element, props) {
|
||||
const { data, width, height, colorScheme, sliceId } = props;
|
||||
const color = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const margin = { ...defaultMargin, ...props.margin };
|
||||
const innerWidth = width - margin.left - margin.right;
|
||||
const innerHeight = height - margin.top - margin.bottom;
|
||||
@@ -107,7 +107,7 @@ function SankeyLoop(element, props) {
|
||||
value / sValue,
|
||||
)})`,
|
||||
)
|
||||
.linkColor(d => color(d.source.name, sliceId));
|
||||
.linkColor(d => colorFn(d.source.name, sliceId));
|
||||
|
||||
const div = select(element);
|
||||
div.selectAll('*').remove();
|
||||
|
||||
@@ -67,7 +67,6 @@ function Sankey(element, props) {
|
||||
.attr('height', innerHeight + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
const sankey = d3Sankey()
|
||||
@@ -219,7 +218,7 @@ function Sankey(element, props) {
|
||||
.attr('width', sankey.nodeWidth())
|
||||
.style('fill', d => {
|
||||
const name = d.name || 'N/A';
|
||||
d.color = colorFn(name, sliceId, colorScheme);
|
||||
d.color = colorFn(name, sliceId);
|
||||
|
||||
return d.color;
|
||||
})
|
||||
|
||||
@@ -43,6 +43,7 @@ const propTypes = {
|
||||
showBubbles: PropTypes.bool,
|
||||
linearColorScheme: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
colorScheme: PropTypes.string,
|
||||
setDataMask: PropTypes.func,
|
||||
onContextMenu: PropTypes.func,
|
||||
emitCrossFilters: PropTypes.bool,
|
||||
@@ -85,24 +86,24 @@ function WorldMap(element, props) {
|
||||
.range([1, maxBubbleSize]);
|
||||
|
||||
let processedData;
|
||||
let colorScale;
|
||||
let colorFn;
|
||||
if (colorBy === ColorBy.Country) {
|
||||
colorScale = CategoricalColorNamespace.getScale(colorScheme);
|
||||
colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
processedData = filteredData.map(d => ({
|
||||
...d,
|
||||
radius: radiusScale(Math.sqrt(d.m2)),
|
||||
fillColor: colorScale(d.name, sliceId),
|
||||
fillColor: colorFn(d.name, sliceId),
|
||||
}));
|
||||
} else {
|
||||
colorScale = getSequentialSchemeRegistry()
|
||||
colorFn = getSequentialSchemeRegistry()
|
||||
.get(linearColorScheme)
|
||||
.createLinearScale(d3Extent(filteredData, d => d.m1));
|
||||
|
||||
processedData = filteredData.map(d => ({
|
||||
...d,
|
||||
radius: radiusScale(Math.sqrt(d.m2)),
|
||||
fillColor: colorScale(d.m1),
|
||||
fillColor: colorFn(d.m1),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -52,16 +52,14 @@ const { getScale } = CategoricalColorNamespace;
|
||||
function getCategories(fd: QueryFormData, data: JsonObject[]) {
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||
const colorFn = getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorFn = getScale(appliedScheme);
|
||||
const categories = {};
|
||||
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, fd.color_scheme),
|
||||
c.a * 255,
|
||||
);
|
||||
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
|
||||
} else {
|
||||
color = fixedColor;
|
||||
}
|
||||
@@ -132,15 +130,13 @@ 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 colorFn = getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorFn = getScale(appliedScheme);
|
||||
|
||||
return data.map(d => {
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
color = hexToRGB(
|
||||
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
|
||||
c.a * 255,
|
||||
);
|
||||
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
|
||||
|
||||
return { ...d, color };
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ export function getLayer(
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
const fd = formData;
|
||||
const colorScale = CategoricalColorNamespace.getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
const colorRange = colorScale
|
||||
.range()
|
||||
.map(color => hexToRGB(color)) as Color[];
|
||||
|
||||
@@ -54,7 +54,8 @@ export function getLayer(
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
const fd = formData;
|
||||
const colorScale = CategoricalColorNamespace.getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
const colorRange = colorScale
|
||||
.range()
|
||||
.map(color => hexToRGB(color)) as Color[];
|
||||
|
||||
@@ -658,9 +658,7 @@ function nvd3Vis(element, props) {
|
||||
} else if (vizType !== 'bullet') {
|
||||
const colorFn = getScale(colorScheme);
|
||||
chart.color(
|
||||
d =>
|
||||
d.color ||
|
||||
colorFn(cleanColorInput(d[colorKey]), sliceId, colorScheme),
|
||||
d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,9 +109,9 @@ export default function transformProps(
|
||||
datum[`${metric}__outliers`],
|
||||
],
|
||||
itemStyle: {
|
||||
color: colorFn(groupbyLabel, sliceId, colorScheme),
|
||||
color: colorFn(groupbyLabel, sliceId),
|
||||
opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6,
|
||||
borderColor: colorFn(groupbyLabel, sliceId, colorScheme),
|
||||
borderColor: colorFn(groupbyLabel, sliceId),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -150,7 +150,7 @@ export default function transformProps(
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
color: colorFn(groupbyLabel, sliceId, colorScheme),
|
||||
color: colorFn(groupbyLabel, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -108,8 +108,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
|
||||
legendOrientation,
|
||||
legendMargin,
|
||||
legendType,
|
||||
sliceId,
|
||||
}: EchartsBubbleFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
|
||||
const legends = new Set<string>();
|
||||
@@ -138,7 +138,10 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
|
||||
],
|
||||
],
|
||||
type: 'scatter',
|
||||
itemStyle: { color: colorFn(name), opacity },
|
||||
itemStyle: {
|
||||
color: colorFn(name, sliceId),
|
||||
opacity,
|
||||
},
|
||||
});
|
||||
legends.add(name);
|
||||
});
|
||||
|
||||
@@ -145,7 +145,6 @@ export default function transformProps(
|
||||
}, {});
|
||||
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getValueFormatter(
|
||||
metric,
|
||||
@@ -175,7 +174,7 @@ export default function transformProps(
|
||||
value,
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(name, sliceId, colorScheme),
|
||||
color: colorFn(name, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -166,6 +166,7 @@ export default function transformProps(
|
||||
const name = groupbyLabels
|
||||
.map(column => `${verboseMap[column] || column}: ${data_point[column]}`)
|
||||
.join(', ');
|
||||
const colorLabel = groupbyLabels.map(col => data_point[col] as string);
|
||||
columnsLabelMap.set(
|
||||
name,
|
||||
groupbyLabels.map(col => data_point[col] as string),
|
||||
@@ -174,7 +175,7 @@ export default function transformProps(
|
||||
value: data_point[metricLabel] as number,
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(index, sliceId, colorScheme),
|
||||
color: colorFn(colorLabel, sliceId),
|
||||
},
|
||||
title: {
|
||||
offsetCenter: [
|
||||
@@ -202,7 +203,7 @@ export default function transformProps(
|
||||
item = {
|
||||
...item,
|
||||
itemStyle: {
|
||||
color: colorFn(index, sliceId, colorScheme),
|
||||
color: colorFn(index, sliceId),
|
||||
opacity: OpacityEnum.SemiTransparent,
|
||||
},
|
||||
detail: {
|
||||
|
||||
@@ -198,6 +198,7 @@ export default function transformProps(
|
||||
const refs: Refs = {};
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const firstColor = colorFn.range()[0];
|
||||
const nodes: { [name: string]: number } = {};
|
||||
const categories: Set<string> = new Set();
|
||||
const echartNodes: EChartGraphNode[] = [];
|
||||
@@ -207,7 +208,12 @@ export default function transformProps(
|
||||
* Get the node id of an existing node,
|
||||
* or create a new node if it doesn't exist.
|
||||
*/
|
||||
function getOrCreateNode(name: string, col: string, category?: string) {
|
||||
function getOrCreateNode(
|
||||
name: string,
|
||||
col: string,
|
||||
category?: string,
|
||||
color?: string,
|
||||
) {
|
||||
if (!(name in nodes)) {
|
||||
nodes[name] = echartNodes.length;
|
||||
echartNodes.push({
|
||||
@@ -221,6 +227,7 @@ export default function transformProps(
|
||||
...getDefaultTooltip(refs),
|
||||
...DEFAULT_GRAPH_SERIES_OPTION.tooltip,
|
||||
},
|
||||
itemStyle: { color },
|
||||
});
|
||||
}
|
||||
const node = echartNodes[nodes[name]];
|
||||
@@ -248,8 +255,25 @@ export default function transformProps(
|
||||
const targetCategoryName = targetCategory
|
||||
? getCategoryName(targetCategory, link[targetCategory])
|
||||
: undefined;
|
||||
const sourceNode = getOrCreateNode(sourceName, source, sourceCategoryName);
|
||||
const targetNode = getOrCreateNode(targetName, target, targetCategoryName);
|
||||
const sourceNodeColor = sourceCategoryName
|
||||
? colorFn(sourceCategoryName)
|
||||
: firstColor;
|
||||
const targetNodeColor = targetCategoryName
|
||||
? colorFn(targetCategoryName)
|
||||
: firstColor;
|
||||
|
||||
const sourceNode = getOrCreateNode(
|
||||
sourceName,
|
||||
source,
|
||||
sourceCategoryName,
|
||||
sourceNodeColor,
|
||||
);
|
||||
const targetNode = getOrCreateNode(
|
||||
targetName,
|
||||
target,
|
||||
targetCategoryName,
|
||||
targetNodeColor,
|
||||
);
|
||||
|
||||
sourceNode.value += value;
|
||||
targetNode.value += value;
|
||||
@@ -258,7 +282,9 @@ export default function transformProps(
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id,
|
||||
value,
|
||||
lineStyle: {},
|
||||
lineStyle: {
|
||||
color: sourceNodeColor,
|
||||
},
|
||||
emphasis: {},
|
||||
select: {},
|
||||
});
|
||||
@@ -271,14 +297,15 @@ export default function transformProps(
|
||||
});
|
||||
|
||||
const categoryList = [...categories];
|
||||
|
||||
const series: GraphSeriesOption[] = [
|
||||
{
|
||||
zoom: DEFAULT_GRAPH_SERIES_OPTION.zoom,
|
||||
type: 'graph',
|
||||
categories: categoryList.map(c => ({
|
||||
name: c,
|
||||
itemStyle: { color: colorFn(c, sliceId, colorScheme) },
|
||||
itemStyle: {
|
||||
color: colorFn(c, sliceId),
|
||||
},
|
||||
})),
|
||||
layout,
|
||||
force: {
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
extractDataTotalValues,
|
||||
extractSeries,
|
||||
extractShowValueIndexes,
|
||||
extractTooltipKeys,
|
||||
getAxisType,
|
||||
getColtypesMapping,
|
||||
getLegendProps,
|
||||
@@ -581,9 +582,13 @@ export default function transformProps(
|
||||
: params.value[0];
|
||||
const forecastValue: any[] = richTooltip ? params : [params];
|
||||
|
||||
if (richTooltip && tooltipSortByMetric) {
|
||||
forecastValue.sort((a, b) => b.data[1] - a.data[1]);
|
||||
}
|
||||
const sortedKeys = extractTooltipKeys(
|
||||
forecastValue,
|
||||
// horizontal mode is not supported in mixed series chart
|
||||
1,
|
||||
richTooltip,
|
||||
tooltipSortByMetric,
|
||||
);
|
||||
|
||||
const rows: string[][] = [];
|
||||
const forecastValues =
|
||||
@@ -591,44 +596,46 @@ export default function transformProps(
|
||||
|
||||
const keys = Object.keys(forecastValues);
|
||||
let focusedRow;
|
||||
keys.forEach(key => {
|
||||
const value = forecastValues[key];
|
||||
// if there are no dimensions, key is a verbose name of a metric,
|
||||
// otherwise it is a comma separated string where the first part is metric name
|
||||
let formatterKey;
|
||||
if (primarySeries.has(key)) {
|
||||
formatterKey =
|
||||
groupby.length === 0 ? inverted[key] : labelMap[key]?.[0];
|
||||
} else {
|
||||
formatterKey =
|
||||
groupbyB.length === 0 ? inverted[key] : labelMapB[key]?.[0];
|
||||
}
|
||||
const tooltipFormatter = getFormatter(
|
||||
customFormatters,
|
||||
formatter,
|
||||
metrics,
|
||||
formatterKey,
|
||||
!!contributionMode,
|
||||
);
|
||||
const tooltipFormatterSecondary = getFormatter(
|
||||
customFormattersSecondary,
|
||||
formatterSecondary,
|
||||
metricsB,
|
||||
formatterKey,
|
||||
!!contributionMode,
|
||||
);
|
||||
const row = formatForecastTooltipSeries({
|
||||
...value,
|
||||
seriesName: key,
|
||||
formatter: primarySeries.has(key)
|
||||
? tooltipFormatter
|
||||
: tooltipFormatterSecondary,
|
||||
sortedKeys
|
||||
.filter(key => keys.includes(key))
|
||||
.forEach(key => {
|
||||
const value = forecastValues[key];
|
||||
// if there are no dimensions, key is a verbose name of a metric,
|
||||
// otherwise it is a comma separated string where the first part is metric name
|
||||
let formatterKey;
|
||||
if (primarySeries.has(key)) {
|
||||
formatterKey =
|
||||
groupby.length === 0 ? inverted[key] : labelMap[key]?.[0];
|
||||
} else {
|
||||
formatterKey =
|
||||
groupbyB.length === 0 ? inverted[key] : labelMapB[key]?.[0];
|
||||
}
|
||||
const tooltipFormatter = getFormatter(
|
||||
customFormatters,
|
||||
formatter,
|
||||
metrics,
|
||||
formatterKey,
|
||||
!!contributionMode,
|
||||
);
|
||||
const tooltipFormatterSecondary = getFormatter(
|
||||
customFormattersSecondary,
|
||||
formatterSecondary,
|
||||
metricsB,
|
||||
formatterKey,
|
||||
!!contributionMode,
|
||||
);
|
||||
const row = formatForecastTooltipSeries({
|
||||
...value,
|
||||
seriesName: key,
|
||||
formatter: primarySeries.has(key)
|
||||
? tooltipFormatter
|
||||
: tooltipFormatterSecondary,
|
||||
});
|
||||
rows.push(row);
|
||||
if (key === focusedSeries) {
|
||||
focusedRow = rows.length - 1;
|
||||
}
|
||||
});
|
||||
rows.push(row);
|
||||
if (key === focusedSeries) {
|
||||
focusedRow = rows.length - 1;
|
||||
}
|
||||
});
|
||||
return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -191,7 +191,6 @@ export default function transformProps(
|
||||
}, {});
|
||||
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getValueFormatter(
|
||||
metric,
|
||||
@@ -223,7 +222,7 @@ export default function transformProps(
|
||||
value,
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(name, sliceId, colorScheme),
|
||||
color: colorFn(name, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -109,7 +109,6 @@ export default function transformProps(
|
||||
...formData,
|
||||
};
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getNumberFormatter(numberFormat);
|
||||
const formatter = (params: CallbackDataParams) =>
|
||||
@@ -166,7 +165,7 @@ export default function transformProps(
|
||||
value: metricLabels.map(metricLabel => datum[metricLabel]),
|
||||
name: joinedName,
|
||||
itemStyle: {
|
||||
color: colorFn(joinedName, sliceId, colorScheme),
|
||||
color: colorFn(joinedName, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.Transparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function transformProps(
|
||||
const refs: Refs = {};
|
||||
const { formData, height, hooks, queriesData, width } = chartProps;
|
||||
const { onLegendStateChanged } = hooks;
|
||||
const { colorScheme, metric, source, target } = formData;
|
||||
const { colorScheme, metric, source, target, sliceId } = formData;
|
||||
const { data } = queriesData[0];
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
@@ -68,7 +68,7 @@ export default function transformProps(
|
||||
).map(name => ({
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(name),
|
||||
color: colorFn(name, sliceId),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
extractDataTotalValues,
|
||||
extractSeries,
|
||||
extractShowValueIndexes,
|
||||
extractTooltipKeys,
|
||||
getAxisType,
|
||||
getColtypesMapping,
|
||||
getLegendProps,
|
||||
@@ -199,7 +200,6 @@ export default function transformProps(
|
||||
}
|
||||
return { ...acc, [entry[0]]: entry[1] };
|
||||
}, {});
|
||||
|
||||
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const rebasedData = rebaseForecastDatum(data, verboseMap);
|
||||
let xAxisLabel = getXAxisLabel(chartProps.rawFormData) as string;
|
||||
@@ -249,7 +249,7 @@ export default function transformProps(
|
||||
legendState,
|
||||
});
|
||||
const seriesContexts = extractForecastSeriesContexts(
|
||||
Object.values(rawSeries).map(series => series.name as string),
|
||||
rawSeries.map(series => series.name as string),
|
||||
);
|
||||
const isAreaExpand = stack === StackControlsValue.Expand;
|
||||
const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig];
|
||||
@@ -539,11 +539,12 @@ export default function transformProps(
|
||||
? params[0].value[xIndex]
|
||||
: params.value[xIndex];
|
||||
const forecastValue: any[] = richTooltip ? params : [params];
|
||||
|
||||
if (richTooltip && tooltipSortByMetric) {
|
||||
forecastValue.sort((a, b) => b.data[yIndex] - a.data[yIndex]);
|
||||
}
|
||||
|
||||
const sortedKeys = extractTooltipKeys(
|
||||
forecastValue,
|
||||
yIndex,
|
||||
richTooltip,
|
||||
tooltipSortByMetric,
|
||||
);
|
||||
const forecastValues: Record<string, ForecastValue> =
|
||||
extractForecastValuesFromTooltipParams(forecastValue, isHorizontal);
|
||||
|
||||
@@ -566,24 +567,28 @@ export default function transformProps(
|
||||
const showPercentage = showTotal && !forcePercentFormatter;
|
||||
const keys = Object.keys(forecastValues);
|
||||
let focusedRow;
|
||||
keys.forEach(key => {
|
||||
const value = forecastValues[key];
|
||||
if (value.observation === 0 && stack) {
|
||||
return;
|
||||
}
|
||||
const row = formatForecastTooltipSeries({
|
||||
...value,
|
||||
seriesName: key,
|
||||
formatter,
|
||||
sortedKeys
|
||||
.filter(key => keys.includes(key))
|
||||
.forEach(key => {
|
||||
const value = forecastValues[key];
|
||||
if (value.observation === 0 && stack) {
|
||||
return;
|
||||
}
|
||||
const row = formatForecastTooltipSeries({
|
||||
...value,
|
||||
seriesName: key,
|
||||
formatter,
|
||||
});
|
||||
if (showPercentage && value.observation !== undefined) {
|
||||
row.push(
|
||||
percentFormatter.format(value.observation / (total || 1)),
|
||||
);
|
||||
}
|
||||
rows.push(row);
|
||||
if (key === focusedSeries) {
|
||||
focusedRow = rows.length - 1;
|
||||
}
|
||||
});
|
||||
if (showPercentage && value.observation !== undefined) {
|
||||
row.push(percentFormatter.format(value.observation / (total || 1)));
|
||||
}
|
||||
rows.push(row);
|
||||
if (key === focusedSeries) {
|
||||
focusedRow = rows.length - 1;
|
||||
}
|
||||
});
|
||||
if (stack) {
|
||||
rows.reverse();
|
||||
if (focusedRow !== undefined) {
|
||||
|
||||
@@ -176,18 +176,18 @@ export default function transformProps(
|
||||
let item: TreemapSeriesNodeItemOption = {
|
||||
name,
|
||||
value,
|
||||
colorSaturation: COLOR_SATURATION,
|
||||
itemStyle: {
|
||||
borderColor: BORDER_COLOR,
|
||||
color: colorFn(name, sliceId),
|
||||
borderWidth: BORDER_WIDTH,
|
||||
gapWidth: GAP_WIDTH,
|
||||
},
|
||||
};
|
||||
if (treeNode.children?.length) {
|
||||
item = {
|
||||
...item,
|
||||
children: traverse(treeNode.children, newPath),
|
||||
colorSaturation: COLOR_SATURATION,
|
||||
itemStyle: {
|
||||
borderColor: BORDER_COLOR,
|
||||
color: colorFn(name, sliceId, colorScheme),
|
||||
borderWidth: BORDER_WIDTH,
|
||||
gapWidth: GAP_WIDTH,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const joinedName = newPath.join(',');
|
||||
@@ -217,7 +217,7 @@ export default function transformProps(
|
||||
colorSaturation: COLOR_SATURATION,
|
||||
itemStyle: {
|
||||
borderColor: BORDER_COLOR,
|
||||
color: colorFn(`${metricLabel}`, sliceId, colorScheme),
|
||||
color: colorFn(`${metricLabel}`, sliceId),
|
||||
borderWidth: BORDER_WIDTH,
|
||||
gapWidth: GAP_WIDTH,
|
||||
},
|
||||
|
||||
@@ -55,6 +55,8 @@ import {
|
||||
ToolboxComponent,
|
||||
GraphicComponent,
|
||||
AriaComponent,
|
||||
MarkAreaComponent,
|
||||
MarkLineComponent,
|
||||
} from 'echarts/components';
|
||||
import { LabelLayout } from 'echarts/features';
|
||||
import { EchartsHandler, EchartsProps, EchartsStylesProps } from '../types';
|
||||
@@ -84,6 +86,8 @@ use([
|
||||
DataZoomComponent,
|
||||
GraphicComponent,
|
||||
GridComponent,
|
||||
MarkAreaComponent,
|
||||
MarkLineComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
TooltipComponent,
|
||||
|
||||
@@ -642,3 +642,22 @@ export function getTimeCompareStackId(
|
||||
}) || defaultId
|
||||
);
|
||||
}
|
||||
|
||||
const TOOLTIP_SERIES_KEY = 'seriesId';
|
||||
export function extractTooltipKeys(
|
||||
forecastValue: any[],
|
||||
yIndex: number,
|
||||
richTooltip?: boolean,
|
||||
tooltipSortByMetric?: boolean,
|
||||
): string[] {
|
||||
if (richTooltip && tooltipSortByMetric) {
|
||||
return forecastValue
|
||||
.slice()
|
||||
.sort((a, b) => b.data[yIndex] - a.data[yIndex])
|
||||
.map(value => value[TOOLTIP_SERIES_KEY]);
|
||||
}
|
||||
if (richTooltip) {
|
||||
return forecastValue.map(s => s[TOOLTIP_SERIES_KEY]);
|
||||
}
|
||||
return [forecastValue[0][TOOLTIP_SERIES_KEY]];
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@ describe('EchartsGraph transformProps', () => {
|
||||
col: 'source_column',
|
||||
category: undefined,
|
||||
id: '0',
|
||||
itemStyle: {
|
||||
color: '#1f77b4',
|
||||
},
|
||||
label: { show: true },
|
||||
name: 'source_value_1',
|
||||
select: {
|
||||
@@ -88,6 +91,9 @@ describe('EchartsGraph transformProps', () => {
|
||||
col: 'target_column',
|
||||
category: undefined,
|
||||
id: '1',
|
||||
itemStyle: {
|
||||
color: '#1f77b4',
|
||||
},
|
||||
label: { show: true },
|
||||
name: 'target_value_1',
|
||||
select: {
|
||||
@@ -102,6 +108,9 @@ describe('EchartsGraph transformProps', () => {
|
||||
col: 'source_column',
|
||||
category: undefined,
|
||||
id: '2',
|
||||
itemStyle: {
|
||||
color: '#1f77b4',
|
||||
},
|
||||
label: { show: true },
|
||||
name: 'source_value_2',
|
||||
select: {
|
||||
@@ -116,6 +125,9 @@ describe('EchartsGraph transformProps', () => {
|
||||
col: 'target_column',
|
||||
category: undefined,
|
||||
id: '3',
|
||||
itemStyle: {
|
||||
color: '#1f77b4',
|
||||
},
|
||||
label: { show: true },
|
||||
name: 'target_value_2',
|
||||
select: {
|
||||
@@ -132,7 +144,7 @@ describe('EchartsGraph transformProps', () => {
|
||||
links: [
|
||||
{
|
||||
emphasis: { lineStyle: { width: 12 } },
|
||||
lineStyle: { width: 6 },
|
||||
lineStyle: { width: 6, color: '#1f77b4' },
|
||||
select: {
|
||||
lineStyle: { opacity: 1, width: 9.600000000000001 },
|
||||
},
|
||||
@@ -142,7 +154,7 @@ describe('EchartsGraph transformProps', () => {
|
||||
},
|
||||
{
|
||||
emphasis: { lineStyle: { width: 5 } },
|
||||
lineStyle: { width: 1.5 },
|
||||
lineStyle: { width: 1.5, color: '#1f77b4' },
|
||||
select: { lineStyle: { opacity: 1, width: 5 } },
|
||||
source: '2',
|
||||
target: '3',
|
||||
@@ -217,6 +229,9 @@ describe('EchartsGraph transformProps', () => {
|
||||
data: [
|
||||
{
|
||||
id: '0',
|
||||
itemStyle: {
|
||||
color: '#1f77b4',
|
||||
},
|
||||
col: 'source_column',
|
||||
name: 'source_value',
|
||||
value: 11,
|
||||
@@ -228,6 +243,9 @@ describe('EchartsGraph transformProps', () => {
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
itemStyle: {
|
||||
color: '#ff7f0e',
|
||||
},
|
||||
col: 'target_column',
|
||||
name: 'target_value',
|
||||
value: 11,
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
extractGroupbyLabel,
|
||||
extractSeries,
|
||||
extractShowValueIndexes,
|
||||
extractTooltipKeys,
|
||||
formatSeriesName,
|
||||
getAxisType,
|
||||
getChartPadding,
|
||||
@@ -1072,3 +1073,29 @@ describe('getTimeCompareStackId', () => {
|
||||
expect(result).toEqual('123');
|
||||
});
|
||||
});
|
||||
|
||||
const forecastValue = [
|
||||
{
|
||||
data: [0, 1],
|
||||
seriesId: 'foo',
|
||||
},
|
||||
{
|
||||
data: [0, 2],
|
||||
seriesId: 'bar',
|
||||
},
|
||||
];
|
||||
|
||||
test('extractTooltipKeys with rich tooltip', () => {
|
||||
const result = extractTooltipKeys(forecastValue, 1, true, false);
|
||||
expect(result).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
test('extractTooltipKeys with rich tooltip and sorting by metrics', () => {
|
||||
const result = extractTooltipKeys(forecastValue, 1, true, true);
|
||||
expect(result).toEqual(['bar', 'foo']);
|
||||
});
|
||||
|
||||
test('extractTooltipKeys with non-rich tooltip', () => {
|
||||
const result = extractTooltipKeys(forecastValue, 1, false, false);
|
||||
expect(result).toEqual(['foo']);
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
SupersetThemeProps,
|
||||
withTheme,
|
||||
seed,
|
||||
CategoricalColorScale,
|
||||
CategoricalColorNamespace,
|
||||
} from '@superset-ui/core';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@@ -230,7 +230,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
encoder.channels.color.setDomainFromDataset(words);
|
||||
|
||||
const { getValueFromDatum } = encoder.channels.color;
|
||||
const colorFn = encoder.channels.color.scale as CategoricalColorScale;
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
const viewBoxWidth = width * scaleFactor;
|
||||
const viewBoxHeight = height * scaleFactor;
|
||||
@@ -250,11 +250,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
fontSize={`${w.size}px`}
|
||||
fontWeight={w.weight}
|
||||
fontFamily={w.font}
|
||||
fill={colorFn(
|
||||
getValueFromDatum(w) as string,
|
||||
sliceId,
|
||||
colorScheme,
|
||||
)}
|
||||
fill={colorFn(getValueFromDatum(w) as string, sliceId)}
|
||||
textAnchor="middle"
|
||||
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
|
||||
>
|
||||
|
||||
@@ -26,10 +26,7 @@ export const getDashboardFormData = (overrides: JsonObject = {}) => ({
|
||||
girl: '#FF69B4',
|
||||
boy: '#ADD8E6',
|
||||
},
|
||||
shared_label_colors: {
|
||||
boy: '#ADD8E6',
|
||||
girl: '#FF69B4',
|
||||
},
|
||||
shared_label_colors: ['boy', 'girl'],
|
||||
color_scheme: 'd3Category20b',
|
||||
extra_filters: [
|
||||
{
|
||||
|
||||
@@ -113,6 +113,6 @@ export const overwriteConfirmMetadata = {
|
||||
slug: null,
|
||||
owners: [],
|
||||
json_metadata:
|
||||
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":{"Male":"#5ac19e","Female":"#1f86c9","<NULL>":"#5AC189","Prefer not to say":"#47457c","No Answer":"#e05043","Yes, an ethnic minority":"#666666","No, not an ethnic minority":"#ffa444","age":"#1FA8C9"},"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
|
||||
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":["Male", "Female","<NULL>","Prefer not to say","No Answer","Yes, an ethnic minority","No, not an ethnic minority","age"],"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -105,7 +105,6 @@ const defaultProps = {
|
||||
const Styles = styled.div`
|
||||
min-height: ${p => p.height}px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
.chart-tooltip {
|
||||
opacity: 0.75;
|
||||
@@ -139,6 +138,7 @@ const LoadingDiv = styled.div`
|
||||
|
||||
const MessageSpan = styled.span`
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: ${({ theme }) => theme.gridUnit * 4}px auto;
|
||||
width: fit-content;
|
||||
color: ${({ theme }) => theme.colors.grayscale.base};
|
||||
|
||||
@@ -16,15 +16,58 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import Slider, { SliderSingleProps } from '.';
|
||||
import Slider, { SliderSingleProps, SliderRangeProps } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Slider',
|
||||
component: Slider,
|
||||
};
|
||||
|
||||
export const InteractiveSlider = (args: SliderSingleProps) => (
|
||||
<Slider {...args} style={{ width: 400, height: 400 }} />
|
||||
const tooltipPlacement = [
|
||||
'top',
|
||||
'left',
|
||||
'bottom',
|
||||
'right',
|
||||
'topLeft',
|
||||
'topRight',
|
||||
'bottomLeft',
|
||||
'bottomRight',
|
||||
'leftTop',
|
||||
'leftBottom',
|
||||
'rightTop',
|
||||
'rightBottom',
|
||||
] as const;
|
||||
|
||||
export const InteractiveSlider = ({
|
||||
tooltipOpen,
|
||||
tooltipPosition,
|
||||
...args
|
||||
}: SliderSingleProps & {
|
||||
tooltipOpen: boolean;
|
||||
tooltipPosition: (typeof tooltipPlacement)[number];
|
||||
}) => (
|
||||
<Slider
|
||||
{...args}
|
||||
tooltip={{
|
||||
...args.tooltip,
|
||||
open: tooltipOpen,
|
||||
placement: tooltipPosition,
|
||||
}}
|
||||
style={{ width: 400, height: 400 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const InteractiveRangeSlider = ({
|
||||
tooltipOpen,
|
||||
draggableTrack,
|
||||
...args
|
||||
}: SliderRangeProps & { tooltipOpen: boolean; draggableTrack: boolean }) => (
|
||||
<Slider
|
||||
{...args}
|
||||
tooltip={{ open: tooltipOpen }}
|
||||
range={{ draggableTrack }}
|
||||
style={{ width: 400, height: 400 }}
|
||||
/>
|
||||
);
|
||||
|
||||
InteractiveSlider.args = {
|
||||
@@ -32,17 +75,33 @@ InteractiveSlider.args = {
|
||||
max: 100,
|
||||
defaultValue: 70,
|
||||
step: 1,
|
||||
marks: {},
|
||||
disabled: false,
|
||||
reverse: false,
|
||||
vertical: false,
|
||||
autoFocus: false,
|
||||
keyboard: true,
|
||||
dots: false,
|
||||
included: true,
|
||||
tooltipPosition: 'bottom',
|
||||
};
|
||||
|
||||
InteractiveSlider.argTypes = {
|
||||
onChange: { action: 'onChange' },
|
||||
disabled: {
|
||||
onChangeComplete: { action: 'onChangeComplete' },
|
||||
tooltipOpen: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
reverse: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
vertical: {
|
||||
control: { type: 'boolean' },
|
||||
tooltipPosition: {
|
||||
options: tooltipPlacement,
|
||||
control: { type: 'select' },
|
||||
},
|
||||
};
|
||||
|
||||
InteractiveRangeSlider.args = {
|
||||
...InteractiveSlider.args,
|
||||
defaultValue: [50, 70],
|
||||
draggableTrack: false,
|
||||
};
|
||||
|
||||
InteractiveRangeSlider.argTypes = InteractiveSlider.argTypes;
|
||||
|
||||
39
superset-frontend/src/components/Slider/Slider.test.tsx
Normal file
39
superset-frontend/src/components/Slider/Slider.test.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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/helpers/testing-library';
|
||||
import Slider from '.';
|
||||
|
||||
const mockedProps = {
|
||||
defaultValue: 90,
|
||||
tooltip: {
|
||||
open: true,
|
||||
},
|
||||
};
|
||||
|
||||
test('should render', () => {
|
||||
const { container } = render(<Slider {...mockedProps} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render with default value on tooltip', () => {
|
||||
render(<Slider {...mockedProps} />);
|
||||
expect(
|
||||
screen.getAllByText(`${mockedProps.defaultValue}`)[0],
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import AntdSlider, {
|
||||
SliderSingleProps,
|
||||
SliderRangeProps,
|
||||
} from 'antd/lib/slider';
|
||||
import { SliderSingleProps, SliderRangeProps } from 'antd-v5/lib/slider';
|
||||
import { Slider as AntdSlider } from 'antd-v5';
|
||||
|
||||
export type { SliderSingleProps, SliderRangeProps };
|
||||
|
||||
export default function Slider(props: SliderSingleProps | SliderRangeProps) {
|
||||
return <AntdSlider {...props} css={{ marginLeft: 0, marginRight: 0 }} />;
|
||||
return <AntdSlider {...props} />;
|
||||
}
|
||||
|
||||
@@ -30,10 +30,13 @@ const loadComponent = (mockCurrentTime?: string) => {
|
||||
return new Promise<FC<TimezoneSelectorProps>>(resolve => {
|
||||
const { default: TimezoneSelector } = module.require('./index');
|
||||
resolve(TimezoneSelector);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('render timezones in correct order for daylight saving time', async () => {
|
||||
const TimezoneSelector = await loadComponent('2022-07-01');
|
||||
const onTimezoneChange = jest.fn();
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import moment from 'moment-timezone';
|
||||
import { FC } from 'react';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { TimezoneSelectorProps } from './index';
|
||||
|
||||
const loadComponent = (mockCurrentTime?: string) => {
|
||||
@@ -30,7 +30,6 @@ const loadComponent = (mockCurrentTime?: string) => {
|
||||
return new Promise<FC<TimezoneSelectorProps>>(resolve => {
|
||||
const { default: TimezoneSelector } = module.require('./index');
|
||||
resolve(TimezoneSelector);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,6 +43,10 @@ const openSelectMenu = () => {
|
||||
|
||||
jest.spyOn(moment.tz, 'guess').mockReturnValue('America/New_York');
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('use the timezone from `moment` if no timezone provided', async () => {
|
||||
const TimezoneSelector = await loadComponent('2022-01-01');
|
||||
const onTimezoneChange = jest.fn();
|
||||
@@ -89,7 +92,6 @@ test('render timezones in correct oder for standard time', async () => {
|
||||
expect(options[0]).toHaveTextContent('GMT -05:00 (Eastern Standard Time)');
|
||||
expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Pago_Pago)');
|
||||
expect(options[2]).toHaveTextContent('GMT -10:00 (Hawaii Standard Time)');
|
||||
expect(options[3]).toHaveTextContent('GMT -10:00 (America/Adak)');
|
||||
});
|
||||
|
||||
test('can select a timezone values and returns canonical timezone name', async () => {
|
||||
@@ -106,7 +108,7 @@ test('can select a timezone values and returns canonical timezone name', async (
|
||||
|
||||
const searchInput = screen.getByRole('combobox');
|
||||
// search for mountain time
|
||||
await userEvent.type(searchInput, 'mou', { delay: 10 });
|
||||
await userEvent.type(searchInput, 'mou');
|
||||
const findTitle = 'GMT -07:00 (Mountain Standard Time)';
|
||||
const selectOption = await screen.findByTitle(findTitle);
|
||||
userEvent.click(selectOption);
|
||||
|
||||
@@ -55,6 +55,7 @@ import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import { safeStringify } from 'src/utils/safeStringify';
|
||||
import { logEvent } from 'src/logger/actions';
|
||||
import { LOG_ACTIONS_CONFIRM_OVERWRITE_DASHBOARD_METADATA } from 'src/logger/LogUtils';
|
||||
import { isEqual } from 'lodash';
|
||||
import { UPDATE_COMPONENTS_PARENTS_LIST } from './dashboardLayout';
|
||||
import {
|
||||
saveChartConfiguration,
|
||||
@@ -68,9 +69,10 @@ import getOverwriteItems from '../util/getOverwriteItems';
|
||||
import {
|
||||
applyColors,
|
||||
isLabelsColorMapSynced,
|
||||
getLabelsColorMapEntries,
|
||||
getColorSchemeDomain,
|
||||
getColorNamespace,
|
||||
getLabelsColorMapEntries,
|
||||
getFreshSharedLabels,
|
||||
} from '../../utils/colorScheme';
|
||||
|
||||
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
|
||||
@@ -224,6 +226,41 @@ export function saveDashboardFinished() {
|
||||
return { type: SAVE_DASHBOARD_FINISHED };
|
||||
}
|
||||
|
||||
export const SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE =
|
||||
'SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE';
|
||||
export const SET_DASHBOARD_LABELS_COLORMAP_SYNCED =
|
||||
'SET_DASHBOARD_LABELS_COLORMAP_SYNCED';
|
||||
export const SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE =
|
||||
'SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE';
|
||||
export const SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED =
|
||||
'SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED';
|
||||
|
||||
export function setDashboardLabelsColorMapSync() {
|
||||
return { type: SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE };
|
||||
}
|
||||
|
||||
export function setDashboardLabelsColorMapSynced() {
|
||||
return { type: SET_DASHBOARD_LABELS_COLORMAP_SYNCED };
|
||||
}
|
||||
|
||||
export function setDashboardSharedLabelsColorsSync() {
|
||||
return { type: SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE };
|
||||
}
|
||||
|
||||
export function setDashboardSharedLabelsColorsSynced() {
|
||||
return { type: SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED };
|
||||
}
|
||||
|
||||
export const setDashboardMetadata = updatedMetadata => async dispatch => {
|
||||
dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata: {
|
||||
...updatedMetadata,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export function saveDashboardRequest(data, id, saveType) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: UPDATE_COMPONENTS_PARENTS_LIST });
|
||||
@@ -254,6 +291,9 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
|
||||
const hasId = item => item.id !== undefined;
|
||||
const metadataCrossFiltersEnabled = data.metadata?.cross_filters_enabled;
|
||||
const colorScheme = data.metadata?.color_scheme;
|
||||
const customLabelsColor = data.metadata?.label_colors || {};
|
||||
const sharedLabelsColor = data.metadata?.shared_label_colors || [];
|
||||
// making sure the data is what the backend expects
|
||||
const cleanedData = {
|
||||
...data,
|
||||
@@ -270,11 +310,14 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
metadata: {
|
||||
...data.metadata,
|
||||
color_namespace: getColorNamespace(data.metadata?.color_namespace),
|
||||
color_scheme: data.metadata?.color_scheme || '',
|
||||
color_scheme_domain: data.metadata?.color_scheme_domain || [],
|
||||
color_scheme: colorScheme || '',
|
||||
color_scheme_domain: colorScheme
|
||||
? getColorSchemeDomain(colorScheme)
|
||||
: [],
|
||||
expanded_slices: data.metadata?.expanded_slices || {},
|
||||
label_colors: data.metadata?.label_colors || {},
|
||||
shared_label_colors: data.metadata?.shared_label_colors || {},
|
||||
label_colors: customLabelsColor,
|
||||
shared_label_colors: getFreshSharedLabels(sharedLabelsColor),
|
||||
map_label_colors: getLabelsColorMapEntries(customLabelsColor),
|
||||
refresh_frequency: data.metadata?.refresh_frequency || 0,
|
||||
timed_refresh_immune_slices:
|
||||
data.metadata?.timed_refresh_immune_slices || [],
|
||||
@@ -324,11 +367,7 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
// syncing with the backend transformations of the metadata
|
||||
if (updatedDashboard.json_metadata) {
|
||||
const metadata = JSON.parse(updatedDashboard.json_metadata);
|
||||
dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata,
|
||||
}),
|
||||
);
|
||||
dispatch(setDashboardMetadata(metadata));
|
||||
if (metadata.chart_configuration) {
|
||||
dispatch({
|
||||
type: SAVE_CHART_CONFIG_COMPLETE,
|
||||
@@ -677,68 +716,261 @@ export function setDatasetsStatus(status) {
|
||||
};
|
||||
}
|
||||
|
||||
const updateDashboardMetadata = async (id, metadata, dispatch) => {
|
||||
await SupersetClient.put({
|
||||
const storeDashboardMetadata = async (id, metadata) =>
|
||||
SupersetClient.put({
|
||||
endpoint: `/api/v1/dashboard/${id}`,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ json_metadata: JSON.stringify(metadata) }),
|
||||
});
|
||||
dispatch(dashboardInfoChanged({ metadata }));
|
||||
};
|
||||
|
||||
export const updateDashboardLabelsColor = () => async (dispatch, getState) => {
|
||||
/**
|
||||
*
|
||||
* Persists the label colors maps in the dashboard metadata.
|
||||
* It runs when outdated color info are detected in stored metadata.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
export const persistDashboardLabelsColor = () => async (dispatch, getState) => {
|
||||
const {
|
||||
dashboardInfo: { id, metadata },
|
||||
dashboardState: { labelsColorMapMustSync, sharedLabelsColorsMustSync },
|
||||
} = getState();
|
||||
const categoricalSchemes = getCategoricalSchemeRegistry();
|
||||
const colorScheme = metadata?.color_scheme;
|
||||
const colorSchemeRegistry = categoricalSchemes.get(
|
||||
metadata?.color_scheme,
|
||||
true,
|
||||
);
|
||||
const defaultScheme = categoricalSchemes.defaultKey;
|
||||
const fallbackScheme = defaultScheme?.toString() || 'supersetColors';
|
||||
const colorSchemeDomain = metadata?.color_scheme_domain || [];
|
||||
|
||||
if (labelsColorMapMustSync || sharedLabelsColorsMustSync) {
|
||||
storeDashboardMetadata(id, metadata);
|
||||
dispatch(setDashboardLabelsColorMapSynced());
|
||||
dispatch(setDashboardSharedLabelsColorsSynced());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the stored dashboard metadata for inconsistencies.
|
||||
* Update the current metadata with validated color information.
|
||||
* It runs only on Dashboard page load.
|
||||
*
|
||||
* @param {*} metadata - the stored dashboard metadata
|
||||
* @returns void
|
||||
*/
|
||||
export const applyDashboardLabelsColorOnLoad = metadata => async dispatch => {
|
||||
try {
|
||||
const updatedMetadata = { ...metadata };
|
||||
let updatedScheme = metadata?.color_scheme;
|
||||
const customLabelsColor = metadata.label_colors || {};
|
||||
const sharedLabelsColor = metadata.shared_label_colors || [];
|
||||
let hasChanged = false;
|
||||
|
||||
// backward compatibility of shared_label_colors
|
||||
const sharedLabels = metadata.shared_label_colors || [];
|
||||
if (!Array.isArray(sharedLabels) && Object.keys(sharedLabels).length > 0) {
|
||||
hasChanged = true;
|
||||
updatedMetadata.shared_label_colors = getFreshSharedLabels(
|
||||
Object.keys(sharedLabelsColor),
|
||||
);
|
||||
}
|
||||
// backward compatibility of map_label_colors
|
||||
const hasMapLabelColors =
|
||||
Object.keys(metadata.map_label_colors || {}).length > 0;
|
||||
|
||||
let updatedScheme = metadata.color_scheme;
|
||||
const categoricalSchemes = getCategoricalSchemeRegistry();
|
||||
const colorSchemeRegistry = categoricalSchemes.get(updatedScheme, true);
|
||||
const hasInvalidColorScheme = !!updatedScheme && !colorSchemeRegistry;
|
||||
|
||||
// color scheme might not exist any longer
|
||||
if (hasInvalidColorScheme) {
|
||||
const defaultScheme = categoricalSchemes.defaultKey;
|
||||
const fallbackScheme = defaultScheme?.toString() || 'supersetColors';
|
||||
hasChanged = true;
|
||||
|
||||
// Color scheme does not exist anymore, fallback to default
|
||||
if (colorScheme && !colorSchemeRegistry) {
|
||||
updatedScheme = fallbackScheme;
|
||||
updatedMetadata.color_scheme = updatedScheme;
|
||||
updatedMetadata.color_scheme_domain = getColorSchemeDomain(colorScheme);
|
||||
|
||||
dispatch(setColorScheme(updatedScheme));
|
||||
// must re-apply colors from fresh labels color map
|
||||
applyColors(updatedMetadata, true);
|
||||
}
|
||||
|
||||
// stored labels color map and applied might differ
|
||||
const isMapSynced = isLabelsColorMapSynced(metadata);
|
||||
if (!isMapSynced) {
|
||||
// re-apply a fresh labels color map
|
||||
applyColors(updatedMetadata, true);
|
||||
// pull and store the just applied labels color map
|
||||
updatedMetadata.shared_label_colors = getLabelsColorMapEntries();
|
||||
}
|
||||
|
||||
// the stored color domain registry and fresh might differ at this point
|
||||
const freshColorSchemeDomain = getColorSchemeDomain(colorScheme);
|
||||
const isRegistrySynced =
|
||||
colorSchemeDomain.toString() !== freshColorSchemeDomain.toString();
|
||||
if (colorScheme && !isRegistrySynced) {
|
||||
const freshColorSchemeDomain = updatedScheme
|
||||
? getColorSchemeDomain(updatedScheme)
|
||||
: [];
|
||||
const currentColorSchemeDomain = metadata.color_scheme_domain || [];
|
||||
|
||||
if (!isEqual(freshColorSchemeDomain, currentColorSchemeDomain)) {
|
||||
hasChanged = true;
|
||||
updatedMetadata.color_scheme_domain = freshColorSchemeDomain;
|
||||
}
|
||||
|
||||
if (
|
||||
(colorScheme && (!colorSchemeRegistry || !isRegistrySynced)) ||
|
||||
!isMapSynced
|
||||
) {
|
||||
await updateDashboardMetadata(id, updatedMetadata, dispatch);
|
||||
// if color scheme is invalid or map is missing, apply a fresh color map
|
||||
// if valid, apply the stored map to keep consistency across refreshes
|
||||
const shouldGoFresh = !hasMapLabelColors || hasInvalidColorScheme;
|
||||
applyColors(updatedMetadata, shouldGoFresh);
|
||||
|
||||
if (shouldGoFresh) {
|
||||
// a fresh color map has been applied
|
||||
// needs to be stored for consistency
|
||||
hasChanged = true;
|
||||
updatedMetadata.map_label_colors =
|
||||
getLabelsColorMapEntries(customLabelsColor);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update dashboard color settings:', error);
|
||||
|
||||
if (hasChanged) {
|
||||
dispatch(setDashboardMetadata(updatedMetadata));
|
||||
dispatch(setDashboardLabelsColorMapSync());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to update dashboard color on load:', e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Ensure that the stored color map matches fresh map.
|
||||
*
|
||||
* @param {*} metadata - the dashboard metadata
|
||||
* @returns void
|
||||
*/
|
||||
export const ensureSyncedLabelsColorMap = metadata => (dispatch, getState) => {
|
||||
const {
|
||||
dashboardState: { labelsColorMapMustSync },
|
||||
} = getState();
|
||||
const updatedMetadata = { ...metadata };
|
||||
const customLabelsColor = metadata.label_colors || {};
|
||||
const isMapSynced = isLabelsColorMapSynced(metadata);
|
||||
const mustSync = !isMapSynced;
|
||||
|
||||
if (mustSync) {
|
||||
const freshestColorMapEntries = getLabelsColorMapEntries(customLabelsColor);
|
||||
updatedMetadata.map_label_colors = freshestColorMapEntries;
|
||||
dispatch(setDashboardMetadata(updatedMetadata));
|
||||
}
|
||||
|
||||
if (mustSync && !labelsColorMapMustSync) {
|
||||
// prepare to persist the just applied labels color map
|
||||
dispatch(setDashboardLabelsColorMapSync());
|
||||
}
|
||||
if (!mustSync && labelsColorMapMustSync) {
|
||||
dispatch(setDashboardLabelsColorMapSynced());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Ensure that the stored shared labels colors match current.
|
||||
*
|
||||
* @param {*} metadata - the dashboard metadata
|
||||
* @returns void
|
||||
*/
|
||||
export const ensureSyncedSharedLabelsColors =
|
||||
metadata => (dispatch, getState) => {
|
||||
// using a timeout to let the rendered charts finish processing labels
|
||||
setTimeout(() => {
|
||||
const {
|
||||
dashboardState: { sharedLabelsColorsMustSync },
|
||||
} = getState();
|
||||
const updatedMetadata = { ...metadata };
|
||||
const sharedLabelsColors = metadata.shared_label_colors || [];
|
||||
const freshLabelsColors = getFreshSharedLabels(sharedLabelsColors);
|
||||
const isSharedLabelsColorsSynced = isEqual(
|
||||
sharedLabelsColors,
|
||||
freshLabelsColors,
|
||||
);
|
||||
|
||||
const mustSync = !isSharedLabelsColorsSynced;
|
||||
|
||||
if (mustSync) {
|
||||
updatedMetadata.shared_label_colors = freshLabelsColors;
|
||||
dispatch(setDashboardMetadata(updatedMetadata));
|
||||
}
|
||||
|
||||
if (mustSync && !sharedLabelsColorsMustSync) {
|
||||
// prepare to persist the shared labels colors
|
||||
dispatch(setDashboardSharedLabelsColorsSync());
|
||||
}
|
||||
if (!mustSync && sharedLabelsColorsMustSync) {
|
||||
dispatch(setDashboardSharedLabelsColorsSynced());
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Updates the color map with new labels and colors as they appear.
|
||||
*
|
||||
* @param {*} renderedChartIds - the charts that have finished rendering
|
||||
* @returns void
|
||||
*/
|
||||
export const updateDashboardLabelsColor =
|
||||
renderedChartIds => (dispatch, getState) => {
|
||||
try {
|
||||
const {
|
||||
dashboardInfo: { metadata },
|
||||
charts,
|
||||
} = getState();
|
||||
const colorScheme = metadata.color_scheme;
|
||||
const labelsColorMapInstance = getLabelsColorMap();
|
||||
const fullLabelsColors = metadata.map_label_colors || {};
|
||||
const sharedLabelsColors = metadata.shared_label_colors || [];
|
||||
const customLabelsColors = metadata.label_colors || {};
|
||||
const updatedMetadata = { ...metadata };
|
||||
|
||||
// for dashboards with no color scheme, the charts should always use their individual schemes
|
||||
// this logic looks for unique labels (not shared across multiple charts) of each rendered chart
|
||||
// it applies a new color to those unique labels when the applied scheme is not up to date
|
||||
// while leaving shared label colors and custom label colors intact for color consistency
|
||||
const shouldReset = [];
|
||||
if (renderedChartIds.length > 0) {
|
||||
const sharedLabelsSet = new Set(sharedLabelsColors);
|
||||
renderedChartIds.forEach(id => {
|
||||
const chart = charts[id];
|
||||
const formData = chart.form_data || chart.latestQueryFormData;
|
||||
// ensure charts have their original color scheme always available
|
||||
labelsColorMapInstance.setOwnColorScheme(
|
||||
formData.slice_id,
|
||||
formData.color_scheme,
|
||||
);
|
||||
|
||||
// if dashboard has a scheme, charts should ignore individual schemes
|
||||
// thus following logic is inapplicable if a dashboard color scheme exists
|
||||
if (colorScheme) return;
|
||||
|
||||
const chartColorScheme = formData.color_scheme;
|
||||
const currentChartConfig = labelsColorMapInstance.chartsLabelsMap.get(
|
||||
formData.slice_id,
|
||||
);
|
||||
const currentChartLabels = currentChartConfig?.labels || [];
|
||||
const uniqueChartLabels = currentChartLabels.filter(
|
||||
l =>
|
||||
!sharedLabelsSet.has(l) && !customLabelsColors.hasOwnProperty(l),
|
||||
);
|
||||
|
||||
// Map unique labels to colors
|
||||
const uniqueChartLabelsColor = new Set(
|
||||
uniqueChartLabels.map(l => fullLabelsColors[l]).filter(Boolean),
|
||||
);
|
||||
|
||||
const expectedColorsForChartScheme = new Set(
|
||||
getColorSchemeDomain(chartColorScheme),
|
||||
);
|
||||
|
||||
// Check if any unique label color is not in the expected colors set
|
||||
const shouldResetColors = [...uniqueChartLabelsColor].some(
|
||||
color => !expectedColorsForChartScheme.has(color),
|
||||
);
|
||||
|
||||
// Only push uniqueChartLabels if they require resetting
|
||||
if (shouldResetColors) shouldReset.push(...uniqueChartLabels);
|
||||
});
|
||||
}
|
||||
|
||||
// an existing map is available, use mrge option
|
||||
// to only apply colors to newly found labels
|
||||
const shouldGoFresh = shouldReset.length > 0 ? shouldReset : false;
|
||||
const shouldMerge = !shouldGoFresh;
|
||||
// re-apply the color map first to get fresh maps accordingly
|
||||
applyColors(updatedMetadata, shouldGoFresh, shouldMerge);
|
||||
// new data may have appeared in the map (data changes)
|
||||
// or new slices may have appeared while changing tabs
|
||||
dispatch(ensureSyncedLabelsColorMap(updatedMetadata));
|
||||
dispatch(ensureSyncedSharedLabelsColors(updatedMetadata));
|
||||
} catch (e) {
|
||||
console.error('Failed to update colors for new charts and labels:', e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -212,7 +212,7 @@ class Dashboard extends PureComponent {
|
||||
|
||||
applyFilters() {
|
||||
const { appliedFilters } = this;
|
||||
const { activeFilters, ownDataCharts, datasources, slices } = this.props;
|
||||
const { activeFilters, ownDataCharts, slices } = this.props;
|
||||
|
||||
// refresh charts if a filter was removed, added, or changed
|
||||
|
||||
@@ -224,6 +224,7 @@ class Dashboard extends PureComponent {
|
||||
ownDataCharts,
|
||||
this.appliedOwnDataCharts,
|
||||
);
|
||||
|
||||
[...allKeys].forEach(filterKey => {
|
||||
if (
|
||||
!currFilterKeys.includes(filterKey) &&
|
||||
@@ -231,22 +232,12 @@ class Dashboard extends PureComponent {
|
||||
) {
|
||||
// filterKey is removed?
|
||||
affectedChartIds.push(
|
||||
...getRelatedCharts(
|
||||
appliedFilters,
|
||||
activeFilters,
|
||||
slices,
|
||||
datasources,
|
||||
)[filterKey],
|
||||
...getRelatedCharts(filterKey, appliedFilters[filterKey], slices),
|
||||
);
|
||||
} else if (!appliedFilterKeys.includes(filterKey)) {
|
||||
// filterKey is newly added?
|
||||
affectedChartIds.push(
|
||||
...getRelatedCharts(
|
||||
activeFilters,
|
||||
appliedFilters,
|
||||
slices,
|
||||
datasources,
|
||||
)[filterKey],
|
||||
...getRelatedCharts(filterKey, activeFilters[filterKey], slices),
|
||||
);
|
||||
} else {
|
||||
// if filterKey changes value,
|
||||
@@ -261,12 +252,7 @@ class Dashboard extends PureComponent {
|
||||
)
|
||||
) {
|
||||
affectedChartIds.push(
|
||||
...getRelatedCharts(
|
||||
activeFilters,
|
||||
appliedFilters,
|
||||
slices,
|
||||
datasources,
|
||||
)[filterKey],
|
||||
...getRelatedCharts(filterKey, activeFilters[filterKey], slices),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -157,9 +157,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh when native filters changed', () => {
|
||||
getRelatedCharts.mockReturnValue({
|
||||
[NATIVE_FILTER_ID]: [230],
|
||||
});
|
||||
getRelatedCharts.mockReturnValue([230]);
|
||||
wrapper.setProps({
|
||||
activeFilters: {
|
||||
...OVERRIDE_FILTERS,
|
||||
@@ -191,13 +189,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh if a filter is added', () => {
|
||||
getRelatedCharts.mockReturnValue({
|
||||
'1_region': [1],
|
||||
'2_country_name': [1, 2],
|
||||
'3_region': [1],
|
||||
'3_country_name': [],
|
||||
gender: [1],
|
||||
});
|
||||
getRelatedCharts.mockReturnValue([1]);
|
||||
const newFilter = {
|
||||
gender: { values: ['boy', 'girl'], scope: [1] },
|
||||
};
|
||||
@@ -209,12 +201,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh if a filter is removed', () => {
|
||||
getRelatedCharts.mockReturnValue({
|
||||
'1_region': [1],
|
||||
'2_country_name': [1, 2],
|
||||
'3_region': [1],
|
||||
'3_country_name': [],
|
||||
});
|
||||
getRelatedCharts.mockReturnValue([]);
|
||||
wrapper.setProps({
|
||||
activeFilters: {},
|
||||
});
|
||||
@@ -223,6 +210,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh if a filter is changed', () => {
|
||||
getRelatedCharts.mockReturnValue([1]);
|
||||
const newFilters = {
|
||||
...OVERRIDE_FILTERS,
|
||||
'1_region': { values: ['Canada'], scope: [1] },
|
||||
@@ -236,6 +224,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh with multiple chart ids', () => {
|
||||
getRelatedCharts.mockReturnValue([1, 2]);
|
||||
const newFilters = {
|
||||
...OVERRIDE_FILTERS,
|
||||
'2_country_name': { values: ['New Country'], scope: [1, 2] },
|
||||
@@ -262,6 +251,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh with empty [] if a filter is changed but scope is not applicable', () => {
|
||||
getRelatedCharts.mockReturnValue([]);
|
||||
const newFilters = {
|
||||
...OVERRIDE_FILTERS,
|
||||
'3_country_name': { values: ['CHINA'], scope: [] },
|
||||
|
||||
@@ -36,6 +36,8 @@ import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
|
||||
|
||||
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
|
||||
|
||||
fetchMock.put('glob:*/api/v1/dashboard/*', {});
|
||||
|
||||
jest.mock('src/dashboard/actions/dashboardState', () => ({
|
||||
...jest.requireActual('src/dashboard/actions/dashboardState'),
|
||||
fetchFaveStar: jest.fn(),
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
// ParentSize uses resize observer so the dashboard will update size
|
||||
// when its container size changes, due to e.g., builder side panel opening
|
||||
import { FC, useEffect, useMemo, useRef } from 'react';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
Filter,
|
||||
@@ -43,12 +43,12 @@ import {
|
||||
import { getChartIdsInFilterScope } from 'src/dashboard/util/getChartIdsInFilterScope';
|
||||
import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId';
|
||||
import { setInScopeStatusOfFilters } from 'src/dashboard/actions/nativeFilters';
|
||||
import { updateDashboardLabelsColor } from 'src/dashboard/actions/dashboardState';
|
||||
import {
|
||||
applyColors,
|
||||
getColorNamespace,
|
||||
resetColors,
|
||||
} from 'src/utils/colorScheme';
|
||||
applyDashboardLabelsColorOnLoad,
|
||||
updateDashboardLabelsColor,
|
||||
persistDashboardLabelsColor,
|
||||
} from 'src/dashboard/actions/dashboardState';
|
||||
import { getColorNamespace, resetColors } from 'src/utils/colorScheme';
|
||||
import { NATIVE_FILTER_DIVIDER_PREFIX } from '../nativeFilters/FiltersConfigModal/utils';
|
||||
import { findTabsWithChartsInScope } from '../nativeFilters/utils';
|
||||
import { getRootLevelTabsComponent } from './utils';
|
||||
@@ -88,6 +88,14 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
const chartIds = useSelector<RootState, number[]>(state =>
|
||||
Object.values(state.charts).map(chart => chart.id),
|
||||
);
|
||||
const renderedChartIds = useSelector<RootState, number[]>(state =>
|
||||
Object.values(state.charts)
|
||||
.filter(chart => chart.chartStatus === 'rendered')
|
||||
.map(chart => chart.id),
|
||||
);
|
||||
const [dashboardLabelsColorInitiated, setDashboardLabelsColorInitiated] =
|
||||
useState(false);
|
||||
const prevRenderedChartIds = useRef<number[]>([]);
|
||||
|
||||
const prevTabIndexRef = useRef();
|
||||
const tabIndex = useMemo(() => {
|
||||
@@ -140,28 +148,65 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
const activeKey = min === 0 ? DASHBOARD_GRID_ID : min.toString();
|
||||
const TOP_OF_PAGE_RANGE = 220;
|
||||
|
||||
const onBeforeUnload = useCallback(() => {
|
||||
dispatch(persistDashboardLabelsColor());
|
||||
resetColors(getColorNamespace(dashboardInfo?.metadata?.color_namespace));
|
||||
prevRenderedChartIds.current = [];
|
||||
}, [dashboardInfo?.metadata?.color_namespace, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// verify freshness of color map on tab change
|
||||
// and when loading for first time
|
||||
setTimeout(() => {
|
||||
dispatch(updateDashboardLabelsColor());
|
||||
}, 500);
|
||||
}, [directPathToChild, dispatch]);
|
||||
// verify freshness of color map
|
||||
// when charts render to catch new labels
|
||||
const numRenderedCharts = renderedChartIds.length;
|
||||
|
||||
if (
|
||||
dashboardLabelsColorInitiated &&
|
||||
dashboardInfo?.id &&
|
||||
numRenderedCharts > 0 &&
|
||||
prevRenderedChartIds.current.length < numRenderedCharts
|
||||
) {
|
||||
const newRenderedChartIds = renderedChartIds.filter(
|
||||
id => !prevRenderedChartIds.current.includes(id),
|
||||
);
|
||||
prevRenderedChartIds.current = renderedChartIds;
|
||||
dispatch(updateDashboardLabelsColor(newRenderedChartIds));
|
||||
}
|
||||
}, [
|
||||
dashboardInfo?.id,
|
||||
renderedChartIds,
|
||||
dispatch,
|
||||
dashboardLabelsColorInitiated,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
const colorNamespace = getColorNamespace(
|
||||
dashboardInfo?.metadata?.color_namespace,
|
||||
);
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
// apply labels color as dictated by stored metadata
|
||||
applyColors(dashboardInfo.metadata);
|
||||
|
||||
if (dashboardInfo?.id && !dashboardLabelsColorInitiated) {
|
||||
// apply labels color as dictated by stored metadata (if any)
|
||||
setDashboardLabelsColorInitiated(true);
|
||||
dispatch(applyDashboardLabelsColorOnLoad(dashboardInfo.metadata));
|
||||
}
|
||||
|
||||
return () => {
|
||||
resetColors(getColorNamespace(colorNamespace));
|
||||
onBeforeUnload();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardInfo.id, dispatch]);
|
||||
}, [dashboardInfo?.id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// 'beforeunload' event interferes with Cypress data cleanup process.
|
||||
// This code prevents 'beforeunload' from triggering in Cypress tests,
|
||||
// as it is not required for end-to-end testing scenarios.
|
||||
if (!(window as any).Cypress) {
|
||||
window.addEventListener('beforeunload', onBeforeUnload);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||
};
|
||||
}, [onBeforeUnload]);
|
||||
|
||||
return (
|
||||
<div className="grid-container" data-test="grid-container">
|
||||
|
||||
@@ -373,3 +373,48 @@ test('should render an extension component if one is supplied', () => {
|
||||
screen.getByText('dashboard.nav.right extension component'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT render MetadataBar when in edit mode', () => {
|
||||
const mockedProps = {
|
||||
...createProps(),
|
||||
editMode: true,
|
||||
dashboardInfo: {
|
||||
...createProps().dashboardInfo,
|
||||
userId: '123',
|
||||
},
|
||||
};
|
||||
setup(mockedProps);
|
||||
expect(
|
||||
screen.queryByText(mockedProps.dashboardInfo.changed_on_delta_humanized),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT render MetadataBar when embedded', () => {
|
||||
const mockedProps = {
|
||||
...createProps(),
|
||||
editMode: false,
|
||||
dashboardInfo: {
|
||||
...createProps().dashboardInfo,
|
||||
userId: undefined,
|
||||
},
|
||||
};
|
||||
setup(mockedProps);
|
||||
expect(
|
||||
screen.queryByText(mockedProps.dashboardInfo.changed_on_delta_humanized),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render MetadataBar when not in edit mode and not embedded', () => {
|
||||
const mockedProps = {
|
||||
...createProps(),
|
||||
editMode: false,
|
||||
dashboardInfo: {
|
||||
...createProps().dashboardInfo,
|
||||
userId: '123',
|
||||
},
|
||||
};
|
||||
setup(mockedProps);
|
||||
expect(
|
||||
screen.getByText(mockedProps.dashboardInfo.changed_on_delta_humanized),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -461,7 +461,6 @@ class Header extends PureComponent {
|
||||
customCss,
|
||||
colorNamespace,
|
||||
dataMask,
|
||||
setColorScheme,
|
||||
setUnsavedChanges,
|
||||
colorScheme,
|
||||
onUndo,
|
||||
@@ -496,11 +495,11 @@ class Header extends PureComponent {
|
||||
const refreshWarning =
|
||||
dashboardInfo.common?.conf
|
||||
?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE;
|
||||
const isEmbedded = !dashboardInfo?.userId;
|
||||
|
||||
const handleOnPropertiesChange = updates => {
|
||||
const { dashboardInfoChanged, dashboardTitleChanged } = this.props;
|
||||
|
||||
setColorScheme(updates.colorScheme);
|
||||
dashboardInfoChanged({
|
||||
slug: updates.slug,
|
||||
metadata: JSON.parse(updates.jsonMetadata || '{}'),
|
||||
@@ -553,7 +552,7 @@ class Header extends PureComponent {
|
||||
visible={!editMode}
|
||||
/>
|
||||
),
|
||||
!editMode && (
|
||||
!editMode && !isEmbedded && (
|
||||
<MetadataBar
|
||||
items={this.getMetadataItems()}
|
||||
tooltipPlacement="bottom"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import { Input } from 'src/components/Input';
|
||||
import { FormItem } from 'src/components/Form';
|
||||
@@ -44,9 +44,19 @@ import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import TagType from 'src/types/TagType';
|
||||
import { fetchTags, OBJECT_TYPES } from 'src/features/tags/tags';
|
||||
import { loadTags } from 'src/components/Tags/utils';
|
||||
import { applyColors, getColorNamespace } from 'src/utils/colorScheme';
|
||||
import {
|
||||
applyColors,
|
||||
getColorNamespace,
|
||||
getLabelsColorMapEntries,
|
||||
} from 'src/utils/colorScheme';
|
||||
import getOwnerName from 'src/utils/getOwnerName';
|
||||
import Owner from 'src/types/Owner';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
setColorScheme,
|
||||
setDashboardMetadata,
|
||||
} from 'src/dashboard/actions/dashboardState';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
|
||||
const StyledFormItem = styled(FormItem)`
|
||||
margin-bottom: 0;
|
||||
@@ -84,6 +94,7 @@ type DashboardInfo = {
|
||||
certifiedBy: string;
|
||||
certificationDetails: string;
|
||||
isManagedExternally: boolean;
|
||||
metadata: Record<string, any>;
|
||||
};
|
||||
|
||||
const PropertiesModal = ({
|
||||
@@ -98,10 +109,11 @@ const PropertiesModal = ({
|
||||
onSubmit = () => {},
|
||||
show = false,
|
||||
}: PropertiesModalProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const [form] = AntdForm.useForm();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
|
||||
const [colorScheme, setColorScheme] = useState(currentColorScheme);
|
||||
const [colorScheme, setCurrentColorScheme] = useState(currentColorScheme);
|
||||
const [jsonMetadata, setJsonMetadata] = useState('');
|
||||
const [dashboardInfo, setDashboardInfo] = useState<DashboardInfo>();
|
||||
const [owners, setOwners] = useState<Owners>([]);
|
||||
@@ -109,6 +121,7 @@ const PropertiesModal = ({
|
||||
const saveLabel = onlyApply ? t('Apply') : t('Save');
|
||||
const [tags, setTags] = useState<TagType[]>([]);
|
||||
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
|
||||
const originalDashboardMetadata = useRef<Record<string, any>>({});
|
||||
|
||||
const tagsAsSelectValues = useMemo(() => {
|
||||
const selectTags = tags.map((tag: { id: number; name: string }) => ({
|
||||
@@ -182,21 +195,24 @@ const PropertiesModal = ({
|
||||
certifiedBy: certified_by || '',
|
||||
certificationDetails: certification_details || '',
|
||||
isManagedExternally: is_managed_externally || false,
|
||||
metadata,
|
||||
};
|
||||
|
||||
form.setFieldsValue(dashboardInfo);
|
||||
setDashboardInfo(dashboardInfo);
|
||||
setOwners(owners);
|
||||
setRoles(roles);
|
||||
setColorScheme(metadata.color_scheme);
|
||||
setCurrentColorScheme(metadata.color_scheme);
|
||||
|
||||
const metaDataCopy = omit(metadata, [
|
||||
'positions',
|
||||
'shared_label_colors',
|
||||
'map_label_colors',
|
||||
'color_scheme_domain',
|
||||
]);
|
||||
|
||||
setJsonMetadata(metaDataCopy ? jsonStringify(metaDataCopy) : '');
|
||||
originalDashboardMetadata.current = metadata;
|
||||
},
|
||||
[form],
|
||||
);
|
||||
@@ -269,6 +285,8 @@ const PropertiesModal = ({
|
||||
return parsedRoles;
|
||||
};
|
||||
|
||||
const handleOnCancel = () => onHide();
|
||||
|
||||
const onColorSchemeChange = (
|
||||
colorScheme = '',
|
||||
{ updateMetadata = true } = {},
|
||||
@@ -287,20 +305,21 @@ const PropertiesModal = ({
|
||||
throw new Error('A valid color scheme is required');
|
||||
}
|
||||
|
||||
jsonMetadataObj.color_scheme = colorScheme;
|
||||
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
|
||||
|
||||
setCurrentColorScheme(colorScheme);
|
||||
dispatch(setColorScheme(colorScheme));
|
||||
|
||||
// update metadata to match selection
|
||||
if (updateMetadata) {
|
||||
jsonMetadataObj.color_scheme = colorScheme;
|
||||
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
|
||||
|
||||
setJsonMetadata(jsonStringify(jsonMetadataObj));
|
||||
}
|
||||
setColorScheme(colorScheme);
|
||||
};
|
||||
|
||||
const onFinish = () => {
|
||||
const { title, slug, certifiedBy, certificationDetails } =
|
||||
form.getFieldsValue();
|
||||
let currentColorScheme = colorScheme;
|
||||
let currentJsonMetadata = jsonMetadata;
|
||||
|
||||
// validate currentJsonMetadata
|
||||
@@ -318,29 +337,48 @@ const PropertiesModal = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const copyMetadata = { ...metadata };
|
||||
const colorNamespace = getColorNamespace(metadata?.color_namespace);
|
||||
|
||||
// color scheme in json metadata has precedence over selection
|
||||
currentColorScheme = metadata?.color_scheme || colorScheme;
|
||||
const updatedColorScheme = metadata?.color_scheme || colorScheme;
|
||||
const shouldGoFresh =
|
||||
updatedColorScheme !== originalDashboardMetadata.current.color_scheme;
|
||||
const shouldResetCustomLabels = !areObjectsEqual(
|
||||
originalDashboardMetadata.current.label_colors || {},
|
||||
metadata?.label_colors || {},
|
||||
);
|
||||
const currentCustomLabels = Object.keys(metadata?.label_colors || {});
|
||||
const prevCustomLabels = Object.keys(
|
||||
originalDashboardMetadata.current.label_colors || {},
|
||||
);
|
||||
const resettableCustomLabels =
|
||||
currentCustomLabels.length > 0 ? currentCustomLabels : prevCustomLabels;
|
||||
const freshCustomLabels =
|
||||
shouldResetCustomLabels && resettableCustomLabels.length > 0
|
||||
? resettableCustomLabels
|
||||
: false;
|
||||
const jsonMetadataObj = getJsonMetadata();
|
||||
const customLabelColors = jsonMetadataObj.label_colors || {};
|
||||
const updatedDashboardMetadata = {
|
||||
...originalDashboardMetadata.current,
|
||||
label_colors: customLabelColors,
|
||||
color_scheme: updatedColorScheme,
|
||||
};
|
||||
|
||||
// remove information from user facing input
|
||||
if (metadata?.shared_label_colors) {
|
||||
delete metadata.shared_label_colors;
|
||||
}
|
||||
if (metadata?.color_scheme_domain) {
|
||||
delete metadata.color_scheme_domain;
|
||||
}
|
||||
originalDashboardMetadata.current = updatedDashboardMetadata;
|
||||
applyColors(updatedDashboardMetadata, shouldGoFresh || freshCustomLabels);
|
||||
dispatch(
|
||||
setDashboardMetadata({
|
||||
...updatedDashboardMetadata,
|
||||
map_label_colors: getLabelsColorMapEntries(customLabelColors),
|
||||
}),
|
||||
);
|
||||
|
||||
// only apply colors, the user has not saved yet
|
||||
applyColors(copyMetadata, true);
|
||||
|
||||
currentJsonMetadata = jsonStringify(metadata);
|
||||
|
||||
onColorSchemeChange(currentColorScheme, {
|
||||
onColorSchemeChange(updatedColorScheme, {
|
||||
updateMetadata: false,
|
||||
});
|
||||
|
||||
currentJsonMetadata = jsonStringify(metadata);
|
||||
|
||||
const moreOnSubmitProps: { roles?: Roles } = {};
|
||||
const morePutProps: { roles?: number[]; tags?: (number | undefined)[] } =
|
||||
{};
|
||||
@@ -557,14 +595,14 @@ const PropertiesModal = ({
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
onHide={handleOnCancel}
|
||||
title={t('Dashboard properties')}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
htmlType="button"
|
||||
buttonSize="small"
|
||||
onClick={onHide}
|
||||
onClick={handleOnCancel}
|
||||
data-test="properties-modal-cancel-button"
|
||||
cta
|
||||
>
|
||||
|
||||
@@ -66,8 +66,8 @@ const SyncDashboardState: FC<Props> = ({ dashboardPageId }) => {
|
||||
>(
|
||||
({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
|
||||
labelsColor: dashboardInfo.metadata?.label_colors || EMPTY_OBJECT,
|
||||
labelsColorMap:
|
||||
dashboardInfo.metadata?.shared_label_colors || EMPTY_OBJECT,
|
||||
labelsColorMap: dashboardInfo.metadata?.map_label_colors || EMPTY_OBJECT,
|
||||
sharedLabelsColors: dashboardInfo.metadata?.shared_label_colors || [],
|
||||
colorScheme: dashboardState?.colorScheme,
|
||||
chartConfiguration:
|
||||
dashboardInfo.metadata?.chart_configuration || EMPTY_OBJECT,
|
||||
|
||||
@@ -197,11 +197,19 @@ class Chart extends Component {
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
// chart should re-render if color scheme or label color was changed
|
||||
// chart should re-render if color scheme or label colors were changed
|
||||
nextProps.formData?.color_scheme !== this.props.formData?.color_scheme ||
|
||||
!areObjectsEqual(
|
||||
nextProps.formData?.label_colors,
|
||||
this.props.formData?.label_colors,
|
||||
nextProps.formData?.label_colors || {},
|
||||
this.props.formData?.label_colors || {},
|
||||
) ||
|
||||
!areObjectsEqual(
|
||||
nextProps.formData?.map_label_colors || {},
|
||||
this.props.formData?.map_label_colors || {},
|
||||
) ||
|
||||
!isEqual(
|
||||
nextProps.formData?.shared_label_colors || [],
|
||||
this.props.formData?.shared_label_colors || [],
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { ResizeCallback, ResizeStartCallback } from 're-resizable';
|
||||
import cx from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { css } from '@superset-ui/core';
|
||||
import { css, useTheme } from '@superset-ui/core';
|
||||
import { LayoutItem, RootState } from 'src/dashboard/types';
|
||||
import AnchorLink from 'src/dashboard/components/AnchorLink';
|
||||
import Chart from 'src/dashboard/containers/Chart';
|
||||
@@ -70,15 +70,6 @@ interface ChartHolderProps {
|
||||
isInView: boolean;
|
||||
}
|
||||
|
||||
const fullSizeStyle = css`
|
||||
&& {
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChartHolder: React.FC<ChartHolderProps> = ({
|
||||
id,
|
||||
parentId,
|
||||
@@ -102,6 +93,16 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
|
||||
setFullSizeChartId,
|
||||
isInView,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const fullSizeStyle = css`
|
||||
&& {
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
`;
|
||||
const { chartId } = component.meta;
|
||||
const isFullSize = fullSizeChartId === chartId;
|
||||
|
||||
|
||||
@@ -16,20 +16,27 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { SyntheticEvent } from 'react';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import downloadAsImage from 'src/utils/downloadAsImage';
|
||||
import DownloadAsImage from './DownloadAsImage';
|
||||
|
||||
const mockAddDangerToast = jest.fn();
|
||||
|
||||
jest.mock('src/utils/downloadAsImage', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => (_e: SyntheticEvent) => {}),
|
||||
}));
|
||||
|
||||
jest.mock('src/components/MessageToasts/withToasts', () => ({
|
||||
useToasts: () => ({
|
||||
addDangerToast: mockAddDangerToast,
|
||||
}),
|
||||
}));
|
||||
|
||||
const createProps = () => ({
|
||||
addDangerToast: jest.fn(),
|
||||
text: 'Download as Image',
|
||||
dashboardTitle: 'Test Dashboard',
|
||||
logEvent: jest.fn(),
|
||||
@@ -40,22 +47,24 @@ const renderComponent = () => {
|
||||
<Menu>
|
||||
<DownloadAsImage {...createProps()} />
|
||||
</Menu>,
|
||||
{
|
||||
useRedux: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
test('Should call download image on click', async () => {
|
||||
const props = createProps();
|
||||
renderComponent();
|
||||
await waitFor(() => {
|
||||
expect(downloadAsImage).toBeCalledTimes(0);
|
||||
expect(props.addDangerToast).toBeCalledTimes(0);
|
||||
expect(mockAddDangerToast).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'Download as Image' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(downloadAsImage).toBeCalledTimes(1);
|
||||
expect(props.addDangerToast).toBeCalledTimes(0);
|
||||
expect(mockAddDangerToast).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -21,20 +21,20 @@ import { logging, t } from '@superset-ui/core';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
|
||||
import downloadAsImage from 'src/utils/downloadAsImage';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
|
||||
export default function DownloadAsImage({
|
||||
text,
|
||||
logEvent,
|
||||
dashboardTitle,
|
||||
addDangerToast,
|
||||
...rest
|
||||
}: {
|
||||
text: string;
|
||||
addDangerToast: Function;
|
||||
dashboardTitle: string;
|
||||
logEvent?: Function;
|
||||
}) {
|
||||
const SCREENSHOT_NODE_SELECTOR = '.dashboard';
|
||||
const { addDangerToast } = useToasts();
|
||||
const onDownloadImage = async (e: SyntheticEvent) => {
|
||||
try {
|
||||
downloadAsImage(SCREENSHOT_NODE_SELECTOR, dashboardTitle, true)(e);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 { SyntheticEvent } from 'react';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import downloadAsPdf from 'src/utils/downloadAsPdf';
|
||||
import DownloadAsPdf from './DownloadAsPdf';
|
||||
|
||||
const mockAddDangerToast = jest.fn();
|
||||
|
||||
jest.mock('src/utils/downloadAsPdf', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => (_e: SyntheticEvent) => {}),
|
||||
}));
|
||||
|
||||
jest.mock('src/components/MessageToasts/withToasts', () => ({
|
||||
useToasts: () => ({
|
||||
addDangerToast: mockAddDangerToast,
|
||||
}),
|
||||
}));
|
||||
|
||||
const createProps = () => ({
|
||||
text: 'Export as PDF',
|
||||
dashboardTitle: 'Test Dashboard',
|
||||
logEvent: jest.fn(),
|
||||
});
|
||||
|
||||
const renderComponent = () => {
|
||||
render(
|
||||
<Menu>
|
||||
<DownloadAsPdf {...createProps()} />
|
||||
</Menu>,
|
||||
{ useRedux: true },
|
||||
);
|
||||
};
|
||||
|
||||
test('Should call download pdf on click', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() => {
|
||||
expect(downloadAsPdf).toHaveBeenCalledTimes(0);
|
||||
expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'Export as PDF' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(downloadAsPdf).toHaveBeenCalledTimes(1);
|
||||
expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('Component is rendered with role="button"', async () => {
|
||||
renderComponent();
|
||||
const button = screen.getByRole('button', { name: 'Export as PDF' });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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 { SyntheticEvent } from 'react';
|
||||
import { logging, t } from '@superset-ui/core';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import downloadAsPdf from 'src/utils/downloadAsPdf';
|
||||
import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF } from 'src/logger/LogUtils';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
|
||||
export default function DownloadAsPdf({
|
||||
text,
|
||||
logEvent,
|
||||
dashboardTitle,
|
||||
...rest
|
||||
}: {
|
||||
text: string;
|
||||
dashboardTitle: string;
|
||||
logEvent?: Function;
|
||||
}) {
|
||||
const SCREENSHOT_NODE_SELECTOR = '.dashboard';
|
||||
const { addDangerToast } = useToasts();
|
||||
const onDownloadPdf = async (e: SyntheticEvent) => {
|
||||
try {
|
||||
downloadAsPdf(SCREENSHOT_NODE_SELECTOR, dashboardTitle, true)(e);
|
||||
} catch (error) {
|
||||
logging.error(error);
|
||||
addDangerToast(t('Sorry, something went wrong. Try again later.'));
|
||||
}
|
||||
logEvent?.(LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item key="download-pdf" {...rest}>
|
||||
<div onClick={onDownloadPdf} role="button" tabIndex={0}>
|
||||
{text}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
@@ -130,6 +130,9 @@ describe('DownloadScreenshot component', () => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddInfoToast).toHaveBeenCalledWith(
|
||||
'The screenshot is being generated. Please, do not leave the page.',
|
||||
{
|
||||
noDuplicate: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -202,7 +205,7 @@ describe('DownloadScreenshot component', () => {
|
||||
// Wait for the successful image retrieval message
|
||||
await waitFor(() => {
|
||||
expect(mockAddSuccessToast).toHaveBeenCalledWith(
|
||||
'The screenshot is now being downloaded.',
|
||||
'The screenshot has been downloaded.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,8 @@ import { RootState } from 'src/dashboard/types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import { last } from 'lodash';
|
||||
import { getDashboardUrlParams } from 'src/utils/urlUtils';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { DownloadScreenshotFormat } from './types';
|
||||
|
||||
const RETRY_INTERVAL = 3000;
|
||||
@@ -52,21 +54,66 @@ export default function DownloadScreenshot({
|
||||
const activeTabs = useSelector(
|
||||
(state: RootState) => state.dashboardState.activeTabs || undefined,
|
||||
);
|
||||
|
||||
const anchor = useSelector(
|
||||
(state: RootState) =>
|
||||
last(state.dashboardState.directPathToChild) || undefined,
|
||||
);
|
||||
|
||||
const dataMask = useSelector(
|
||||
(state: RootState) => state.dataMask || undefined,
|
||||
);
|
||||
|
||||
const { addDangerToast, addSuccessToast, addInfoToast } = useToasts();
|
||||
const currentIntervalIds = useRef<NodeJS.Timeout[]>([]);
|
||||
|
||||
const printLoadingToast = () =>
|
||||
addInfoToast(
|
||||
t('The screenshot is being generated. Please, do not leave the page.'),
|
||||
{
|
||||
noDuplicate: true,
|
||||
},
|
||||
);
|
||||
|
||||
const printFailureToast = useCallback(
|
||||
() =>
|
||||
addDangerToast(
|
||||
t('The screenshot could not be downloaded. Please, try again later.'),
|
||||
),
|
||||
[addDangerToast],
|
||||
);
|
||||
|
||||
const printSuccessToast = useCallback(
|
||||
() => addSuccessToast(t('The screenshot has been downloaded.')),
|
||||
[addSuccessToast],
|
||||
);
|
||||
|
||||
const stopIntervals = useCallback(
|
||||
(message?: 'success' | 'failure') => {
|
||||
currentIntervalIds.current.forEach(clearInterval);
|
||||
|
||||
if (message === 'failure') {
|
||||
printFailureToast();
|
||||
}
|
||||
if (message === 'success') {
|
||||
printSuccessToast();
|
||||
}
|
||||
},
|
||||
[printFailureToast, printSuccessToast],
|
||||
);
|
||||
|
||||
const onDownloadScreenshot = () => {
|
||||
let retries = 0;
|
||||
|
||||
const toastIntervalId = setInterval(
|
||||
() => printLoadingToast(),
|
||||
RETRY_INTERVAL,
|
||||
);
|
||||
|
||||
currentIntervalIds.current = [
|
||||
...(currentIntervalIds.current || []),
|
||||
toastIntervalId,
|
||||
];
|
||||
|
||||
printLoadingToast();
|
||||
|
||||
// this function checks if the image is ready
|
||||
const checkImageReady = (cacheKey: string) =>
|
||||
SupersetClient.get({
|
||||
@@ -84,6 +131,7 @@ export default function DownloadScreenshot({
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
stopIntervals('success');
|
||||
})
|
||||
.catch(err => {
|
||||
if ((err as SupersetApiError).status === 404) {
|
||||
@@ -91,34 +139,15 @@ export default function DownloadScreenshot({
|
||||
}
|
||||
});
|
||||
|
||||
// this is the functions that handles the retries
|
||||
const fetchImageWithRetry = (cacheKey: string) => {
|
||||
checkImageReady(cacheKey)
|
||||
.then(() => {
|
||||
addSuccessToast(t('The screenshot is now being downloaded.'));
|
||||
})
|
||||
.catch(error => {
|
||||
// we check how many retries have been made
|
||||
if (retries < MAX_RETRIES) {
|
||||
retries += 1;
|
||||
addInfoToast(
|
||||
t(
|
||||
'The screenshot is being generated. Please, do not leave the page.',
|
||||
),
|
||||
{
|
||||
noDuplicate: true,
|
||||
},
|
||||
);
|
||||
setTimeout(() => fetchImageWithRetry(cacheKey), RETRY_INTERVAL);
|
||||
} else {
|
||||
addDangerToast(
|
||||
t(
|
||||
'The screenshot could not be downloaded. Please, try again later.',
|
||||
),
|
||||
);
|
||||
logging.error(error);
|
||||
}
|
||||
});
|
||||
if (retries >= MAX_RETRIES) {
|
||||
stopIntervals('failure');
|
||||
logging.error('Max retries reached');
|
||||
return;
|
||||
}
|
||||
checkImageReady(cacheKey).catch(() => {
|
||||
retries += 1;
|
||||
});
|
||||
};
|
||||
|
||||
SupersetClient.post({
|
||||
@@ -127,6 +156,7 @@ export default function DownloadScreenshot({
|
||||
anchor,
|
||||
activeTabs,
|
||||
dataMask,
|
||||
urlParams: getDashboardUrlParams(['edit']),
|
||||
},
|
||||
})
|
||||
.then(({ json }) => {
|
||||
@@ -134,18 +164,15 @@ export default function DownloadScreenshot({
|
||||
if (!cacheKey) {
|
||||
throw new Error('No image URL in response');
|
||||
}
|
||||
addInfoToast(
|
||||
t(
|
||||
'The screenshot is being generated. Please, do not leave the page.',
|
||||
),
|
||||
);
|
||||
const retryIntervalId = setInterval(() => {
|
||||
fetchImageWithRetry(cacheKey);
|
||||
}, RETRY_INTERVAL);
|
||||
currentIntervalIds.current.push(retryIntervalId);
|
||||
fetchImageWithRetry(cacheKey);
|
||||
})
|
||||
.catch(error => {
|
||||
logging.error(error);
|
||||
addDangerToast(
|
||||
t('The screenshot could not be downloaded. Please, try again later.'),
|
||||
);
|
||||
stopIntervals('failure');
|
||||
})
|
||||
.finally(() => {
|
||||
logEvent?.(
|
||||
@@ -156,6 +183,16 @@ export default function DownloadScreenshot({
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (currentIntervalIds.current.length > 0) {
|
||||
stopIntervals();
|
||||
}
|
||||
currentIntervalIds.current = [];
|
||||
},
|
||||
[stopIntervals],
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu.Item key={format} {...rest}>
|
||||
<div onClick={onDownloadScreenshot} role="button" tabIndex={0}>
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
|
||||
import DownloadScreenshot from './DownloadScreenshot';
|
||||
import { DownloadScreenshotFormat } from './types';
|
||||
import DownloadAsPdf from './DownloadAsPdf';
|
||||
import DownloadAsImage from './DownloadAsImage';
|
||||
|
||||
export interface DownloadMenuItemProps {
|
||||
pdfMenuItemTitle: string;
|
||||
@@ -34,25 +37,48 @@ const DownloadMenuItems = (props: DownloadMenuItemProps) => {
|
||||
imageMenuItemTitle,
|
||||
logEvent,
|
||||
dashboardId,
|
||||
dashboardTitle,
|
||||
...rest
|
||||
} = props;
|
||||
const isWebDriverScreenshotEnabled =
|
||||
isFeatureEnabled(FeatureFlag.EnableDashboardScreenshotEndpoints) &&
|
||||
isFeatureEnabled(FeatureFlag.EnableDashboardDownloadWebDriverScreenshot);
|
||||
|
||||
return (
|
||||
<Menu selectable={false}>
|
||||
<DownloadScreenshot
|
||||
text={pdfMenuItemTitle}
|
||||
dashboardId={dashboardId}
|
||||
logEvent={logEvent}
|
||||
format={DownloadScreenshotFormat.PDF}
|
||||
{...rest}
|
||||
/>
|
||||
<DownloadScreenshot
|
||||
text={imageMenuItemTitle}
|
||||
dashboardId={dashboardId}
|
||||
logEvent={logEvent}
|
||||
format={DownloadScreenshotFormat.PNG}
|
||||
{...rest}
|
||||
/>
|
||||
{isWebDriverScreenshotEnabled ? (
|
||||
<>
|
||||
<DownloadScreenshot
|
||||
text={pdfMenuItemTitle}
|
||||
dashboardId={dashboardId}
|
||||
logEvent={logEvent}
|
||||
format={DownloadScreenshotFormat.PDF}
|
||||
{...rest}
|
||||
/>
|
||||
<DownloadScreenshot
|
||||
text={imageMenuItemTitle}
|
||||
dashboardId={dashboardId}
|
||||
logEvent={logEvent}
|
||||
format={DownloadScreenshotFormat.PNG}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DownloadAsPdf
|
||||
text={pdfMenuItemTitle}
|
||||
dashboardTitle={dashboardTitle}
|
||||
logEvent={logEvent}
|
||||
{...rest}
|
||||
/>
|
||||
<DownloadAsImage
|
||||
text={imageMenuItemTitle}
|
||||
dashboardTitle={dashboardTitle}
|
||||
logEvent={logEvent}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ export const crossFiltersSelector = (props: {
|
||||
verboseMaps: { [key: string]: Record<string, string> };
|
||||
}): CrossFilterIndicator[] => {
|
||||
const { dataMask, chartConfiguration, dashboardLayout, verboseMaps } = props;
|
||||
const chartsIds = Object.keys(chartConfiguration);
|
||||
const chartsIds = Object.keys(chartConfiguration || {});
|
||||
|
||||
return chartsIds
|
||||
.map(chartId => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user