Compare commits

...

27 Commits

Author SHA1 Message Date
Maxime Beauchemin
774910f40d touching superset/ 2024-12-02 12:09:33 -08:00
Maxime Beauchemin
dbbcc11a98 fix: add python-base layer to fix CI builds in 4.1 2024-12-02 12:02:09 -08:00
Geido
cebd45778f fix(Dashboard): Native & Cross-Filters Scoping Performance (#30881)
(cherry picked from commit f4c36a6d05)
2024-11-22 11:23:37 -08:00
Linden
1b2ecc6955 fix(imports): import query_context for imports with charts (#30887)
(cherry picked from commit 8905508d8f)
2024-11-22 11:19:02 -08:00
JUST.in DO IT
1f5e567645 fix(explore): verified props is not updated (#31008)
(cherry picked from commit 9e5b568cc9)
2024-11-22 11:18:33 -08:00
Geido
c467fb566d fix(Dashboard): Retain colors when color scheme not set (#30646)
(cherry picked from commit 90572be95a)
2024-11-22 11:17:55 -08:00
Geido
cc0ed0fef4 fix(Dashboard): Exclude edit param in async screenshot (#30962)
(cherry picked from commit 1b63b8f3c7)
2024-11-22 11:17:17 -08:00
yousoph
4f463399a7 docs: Updating 4.1 Release Notes (#30865)
(cherry picked from commit 88eb95c39a)
2024-11-22 11:16:43 -08:00
Joe Li
6264ff5165 chore: Adds 4.1.1 RC1 data to CHANGELOG.md and update frontend package 2024-11-15 14:35:11 -08:00
Sukuna
1410e528a4 fix: blocks UI elements on right side (#30886)
Co-authored-by: Evan Rusackas <evan@preset.io>
(cherry picked from commit df479940a6)
2024-11-15 09:27:39 -08:00
Geido
f704b0f556 fix(package.json): Pin luxon version to unblock master (#30859)
(cherry picked from commit de8282cea0)
2024-11-13 15:50:29 -08:00
Maxime Beauchemin
bdfd5cd4ec fix(explore): column data type tooltip format (#30588)
(cherry picked from commit 73768f6313)
2024-11-13 15:48:42 -08:00
Elizabeth Thompson
af44b14fbe chore: add link to Superset when report error (#30576)
(cherry picked from commit 4d5f70c694)
2024-11-13 12:23:27 -08:00
Ayush Tripathi
29c76ef1d5 fix: Rename database from 'couchbasedb' to 'couchbase' in documentation and db_engine_specs (#29911)
(cherry picked from commit f5d614d80d)
2024-11-13 12:21:04 -08:00
Geido
7953c89d51 fix(TimezoneSelector): Failing unit tests due to timezone change (#30828)
(cherry picked from commit 5820d31b5c)
2024-11-13 12:17:14 -08:00
Joe Li
fd4c3dce44 fix: don't show metadata for embedded dashboards (#30875)
(cherry picked from commit ac3a10d8f1)
2024-11-13 12:10:59 -08:00
Michael S. Molina
234f8c94d1 fix: Graph chart colors (#30851)
(cherry picked from commit 0e165c1a21)
2024-11-13 12:10:17 -08:00
Evan Rusackas
bc2e51d8d0 fix(capitalization): Capitalizing a button. (#29867)
(cherry picked from commit 052b38bdf3)
2024-11-13 12:08:26 -08:00
Geido
2787167abe refactor(Slider): Upgrade Slider to Antd 5 (#29786)
(cherry picked from commit d877d46557)
2024-11-13 12:07:26 -08:00
Ross Mabbett
9e84e13888 refactor(ChartCreation): Migrate tests to RTL (#29674)
(cherry picked from commit 6bc8567802)
2024-11-13 12:06:44 -08:00
Ross Mabbett
cfd24e3ccd refactor(controls): Migrate AdhocMetricOption.test to RTL (#29843)
(cherry picked from commit 819597faf6)
2024-11-13 12:06:19 -08:00
Ross Mabbett
aaecec2e03 refactor(controls): Migrate MetricDefinitionValue.test to RTL (#29845)
(cherry picked from commit 27c08d0e0e)
2024-11-13 12:05:47 -08:00
Evan Rusackas
66fe0b0594 fix(translations): Translate embedded errors (#29782)
(cherry picked from commit 0d62bb2261)
2024-11-13 12:01:13 -08:00
Evan Rusackas
0d0b43062e fix: Fixing incomplete string escaping. (#29772)
(cherry picked from commit 2bce20f790)
2024-11-13 12:00:03 -08:00
Đỗ Trọng Hải
72df46a729 fix(frontend/docker, ci): fix borked Docker build due to Lerna v8 uplift (#29725)
Signed-off-by: hainenber <dotronghai96@gmail.com>
(cherry picked from commit 8891f04f11)
2024-11-13 11:58:44 -08:00
Evan Rusackas
f7cfd9182a docs: Check markdown files for bad links using linkinator (#28424)
(cherry picked from commit c3702be9d4)
2024-11-13 11:57:22 -08:00
Sam Firke
5faaaf978b docs(contributing): fix broken link to translations sub-section (#29768)
(cherry picked from commit c22dfa1abb)
2024-11-13 11:56:47 -08:00
145 changed files with 4162 additions and 1168 deletions

View File

@@ -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

50
CHANGELOG/4.1.1.md Normal file
View 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)

View File

@@ -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/)

View File

@@ -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 \

View File

@@ -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

View File

@@ -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)

View File

@@ -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!

View File

@@ -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%">

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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}
```

View File

@@ -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

View File

@@ -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 youre only fixing a small bug, its fine to submit a pull request right away but we highly recommend to file an issue detailing what youre fixing. This is helpful in case we dont 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 youre only fixing a small bug, its fine to submit a pull request right away but we highly recommend filing an issue detailing what youre fixing. This is helpful in case we dont 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.

View File

@@ -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

View File

@@ -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');

View File

@@ -91,7 +91,7 @@ export function prepareDashboardFilters(
refresh_frequency: 0,
color_scheme: '',
label_colors: {},
shared_label_colors: {},
shared_label_colors: [],
color_scheme_domain: [],
cross_filters_enabled: false,
positions: {

View File

@@ -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');
}
@@ -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) => {

View File

@@ -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/*",
@@ -83,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",
@@ -93,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",
@@ -41662,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"
}
@@ -99673,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",

View File

@@ -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",
@@ -148,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",
@@ -158,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",

View File

@@ -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 = (

View File

@@ -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'> = {

View File

@@ -87,6 +87,9 @@ export interface Dataset {
}
export interface ControlPanelState {
slice: {
slice_id: number;
};
form_data: QueryFormData;
datasource: Dataset | QueryResponse | null;
controls: ControlStateMapping;

View File

@@ -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 || [],
};
};

View File

@@ -27,3 +27,4 @@ export * from './defineSavedMetrics';
export * from './getStandardizedControls';
export * from './getTemporalColumns';
export { default as displayTimeRelatedControls } from './displayTimeRelatedControls';
export * from './colorControls';

View File

@@ -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();
});

View File

@@ -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: [],
});
});
});

View File

@@ -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);
}

View File

@@ -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);
}
/**

View File

@@ -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);

View File

@@ -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' });
});
});
});

View File

@@ -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');
});
});

View File

@@ -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);
});
});
});

View File

@@ -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.

View File

@@ -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,

View File

@@ -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 (

View File

@@ -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;
});

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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;
})

View File

@@ -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),
}));
}

View File

@@ -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 };
}

View File

@@ -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[];

View File

@@ -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[];

View File

@@ -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),
);
}

View File

@@ -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,

View File

@@ -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);
});

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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),
},
}));

View File

@@ -200,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;

View File

@@ -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,
},

View File

@@ -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,

View File

@@ -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})`}
>

View File

@@ -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: [
{

View File

@@ -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":{}}',
},
};

View File

@@ -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;

View 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();
});

View File

@@ -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} />;
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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);
}
};

View File

@@ -224,6 +224,7 @@ class Dashboard extends PureComponent {
ownDataCharts,
this.appliedOwnDataCharts,
);
[...allKeys].forEach(filterKey => {
if (
!currFilterKeys.includes(filterKey) &&
@@ -231,12 +232,12 @@ class Dashboard extends PureComponent {
) {
// filterKey is removed?
affectedChartIds.push(
...getRelatedCharts(appliedFilters, activeFilters, slices)[filterKey],
...getRelatedCharts(filterKey, appliedFilters[filterKey], slices),
);
} else if (!appliedFilterKeys.includes(filterKey)) {
// filterKey is newly added?
affectedChartIds.push(
...getRelatedCharts(activeFilters, appliedFilters, slices)[filterKey],
...getRelatedCharts(filterKey, activeFilters[filterKey], slices),
);
} else {
// if filterKey changes value,
@@ -251,9 +252,7 @@ class Dashboard extends PureComponent {
)
) {
affectedChartIds.push(
...getRelatedCharts(activeFilters, appliedFilters, slices)[
filterKey
],
...getRelatedCharts(filterKey, activeFilters[filterKey], slices),
);
}

View File

@@ -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: [] },

View File

@@ -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(),

View File

@@ -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">

View File

@@ -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();
});

View File

@@ -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"

View File

@@ -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
>

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -156,7 +156,7 @@ export default function DownloadScreenshot({
anchor,
activeTabs,
dataMask,
urlParams: getDashboardUrlParams(),
urlParams: getDashboardUrlParams(['edit']),
},
})
.then(({ json }) => {

View File

@@ -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 => {

View File

@@ -41,7 +41,8 @@ const initialState: { dashboardInfo: DashboardInfo } = {
color_namespace: '',
color_scheme_domain: [],
label_colors: {},
shared_label_colors: {},
shared_label_colors: [],
map_label_colors: {},
cross_filters_enabled: false,
},
json_metadata: '',

View File

@@ -485,7 +485,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
Sports: '#8FD3E4',
Strategy: '#A1A6BD',
},
shared_label_colors: {},
shared_label_colors: [],
color_scheme: 'supersetColors',
extra_filters: [
{
@@ -710,7 +710,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
queryFields: {
metric: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
color_scheme: 'supersetColors',
extra_filters: [
{
@@ -759,7 +759,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
queryFields: {
metric: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
color_scheme: 'supersetColors',
dashboardId: 9,
applied_time_extras: {},
@@ -3046,7 +3046,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
extra_filters: [
{
col: '__time_range',
@@ -3125,7 +3125,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
dashboardId: 9,
applied_time_extras: {},
where: '',
@@ -16514,7 +16514,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
extra_filters: [
{
col: '__time_range',
@@ -16661,7 +16661,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
dashboardId: 9,
applied_time_extras: {},
where: '',
@@ -17523,7 +17523,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
Sports: '#8FD3E4',
Strategy: '#A1A6BD',
},
shared_label_colors: {},
shared_label_colors: [],
extra_filters: [
{
col: '__time_range',
@@ -17678,7 +17678,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
extra_filters: [
{
col: '__time_range',
@@ -17715,7 +17715,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
dashboardId: 9,
applied_time_extras: {},
where: '',

View File

@@ -59,17 +59,24 @@ function mapStateToProps(
const datasource =
(chart && chart.form_data && datasources[chart.form_data.datasource]) ||
PLACEHOLDER_DATASOURCE;
const { colorScheme, colorNamespace, datasetsStatus } = dashboardState;
const {
colorScheme: appliedColorScheme,
colorNamespace,
datasetsStatus,
} = dashboardState;
const labelsColor = dashboardInfo?.metadata?.label_colors || {};
const labelsColorMap = dashboardInfo?.metadata?.shared_label_colors || {};
const labelsColorMap = dashboardInfo?.metadata?.map_label_colors || {};
const sharedLabelsColors = dashboardInfo?.metadata?.shared_label_colors || [];
const ownColorScheme = chart.form_data?.color_scheme;
// note: this method caches filters if possible to prevent render cascades
const formData = getFormDataWithExtraFilters({
chart,
chartConfiguration: dashboardInfo.metadata?.chart_configuration,
charts: chartQueries,
filters: getAppliedFilterValues(id),
colorScheme,
colorNamespace,
colorScheme: appliedColorScheme,
ownColorScheme,
sliceId: id,
nativeFilters: nativeFilters?.filters,
allSliceIds: dashboardState.sliceIds,
@@ -77,6 +84,7 @@ function mapStateToProps(
extraControls,
labelsColor,
labelsColorMap,
sharedLabelsColors,
});
formData.dashboardId = dashboardInfo.id;

View File

@@ -46,6 +46,10 @@ import {
SET_OVERRIDE_CONFIRM,
SAVE_DASHBOARD_STARTED,
SAVE_DASHBOARD_FINISHED,
SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE,
SET_DASHBOARD_LABELS_COLORMAP_SYNCED,
SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE,
SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED,
} from '../actions/dashboardState';
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
@@ -101,6 +105,30 @@ export default function dashboardStateReducer(state = {}, action) {
updatedColorScheme: true,
};
},
[SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE]() {
return {
...state,
labelsColorMapMustSync: true,
};
},
[SET_DASHBOARD_LABELS_COLORMAP_SYNCED]() {
return {
...state,
labelsColorMapMustSync: false,
};
},
[SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE]() {
return {
...state,
sharedLabelsColorsMustSync: true,
};
},
[SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED]() {
return {
...state,
sharedLabelsColorsMustSync: false,
};
},
[TOGGLE_EXPAND_SLICE]() {
const updatedExpandedSlices = { ...state.expandedSlices };
const { sliceId } = action;

View File

@@ -52,6 +52,8 @@ export type Chart = ChartState & {
form_data: {
viz_type: string;
datasource: string;
color_scheme: string;
slice_id: number;
};
};
@@ -131,7 +133,8 @@ export type DashboardInfo = {
color_namespace: string;
color_scheme_domain: string[];
label_colors: JsonObject;
shared_label_colors: JsonObject;
shared_label_colors: string[];
map_label_colors: JsonObject;
cross_filters_enabled: boolean;
};
crossFiltersEnabled: boolean;

View File

@@ -25,6 +25,7 @@ import {
import { ChartConfiguration, ChartQueryPayload } from 'src/dashboard/types';
import { getExtraFormData } from 'src/dashboard/components/nativeFilters/utils';
import { areObjectsEqual } from 'src/reduxUtils';
import { isEqual } from 'lodash';
import getEffectiveExtraFilters from './getEffectiveExtraFilters';
import { getAllActiveFilters } from '../activeAllDashboardFilters';
@@ -38,6 +39,7 @@ export interface GetFormDataWithExtraFiltersArguments {
chart: ChartQueryPayload;
filters: DataRecordFilters;
colorScheme?: string;
ownColorScheme?: string;
colorNamespace?: string;
sliceId: number;
dataMask: DataMaskStateWithId;
@@ -45,6 +47,7 @@ export interface GetFormDataWithExtraFiltersArguments {
extraControls: Record<string, string | boolean | null>;
labelsColor?: Record<string, string>;
labelsColorMap?: Record<string, string>;
sharedLabelsColors?: string[];
allSliceIds: number[];
}
@@ -57,30 +60,32 @@ export default function getFormDataWithExtraFilters({
nativeFilters,
chartConfiguration,
colorScheme,
ownColorScheme,
colorNamespace,
sliceId,
dataMask,
extraControls,
labelsColor,
labelsColorMap,
sharedLabelsColors,
allSliceIds,
}: GetFormDataWithExtraFiltersArguments) {
// if dashboard metadata + filters have not changed, use cache if possible
const cachedFormData = cachedFormdataByChart[sliceId];
if (
cachedFiltersByChart[sliceId] === filters &&
areObjectsEqual(cachedFormData?.color_scheme, colorScheme, {
ignoreUndefined: true,
}) &&
areObjectsEqual(cachedFormData?.own_color_scheme, ownColorScheme) &&
areObjectsEqual(cachedFormData?.color_scheme, colorScheme) &&
areObjectsEqual(cachedFormData?.color_namespace, colorNamespace, {
ignoreUndefined: true,
}) &&
areObjectsEqual(cachedFormData?.label_colors, labelsColor, {
ignoreUndefined: true,
}) &&
areObjectsEqual(cachedFormData?.shared_label_colors, labelsColorMap, {
areObjectsEqual(cachedFormData?.map_label_colors, labelsColorMap, {
ignoreUndefined: true,
}) &&
isEqual(cachedFormData?.shared_label_colors, sharedLabelsColors) &&
!!cachedFormData &&
areObjectsEqual(cachedFormData?.dataMask, dataMask, {
ignoreUndefined: true,
@@ -110,9 +115,14 @@ export default function getFormDataWithExtraFilters({
const formData = {
...chart.form_data,
chart_id: chart.id,
label_colors: labelsColor,
shared_label_colors: labelsColorMap,
shared_label_colors: sharedLabelsColors,
map_label_colors: labelsColorMap,
...(colorScheme && { color_scheme: colorScheme }),
...(ownColorScheme && {
own_color_scheme: ownColorScheme,
}),
extra_filters: getEffectiveExtraFilters(filters),
...extraData,
...extraControls,

View File

@@ -58,6 +58,7 @@ const CHARTS = {
datasource: '2__table',
viz_type: 'echarts_timeseries_line',
slice_id: 1,
color_scheme: 'supersetColors',
},
chartAlert: null,
chartStatus: 'rendered' as const,
@@ -76,6 +77,7 @@ const CHARTS = {
'2': {
id: 2,
form_data: {
color_scheme: 'supersetColors',
datasource: '2__table',
viz_type: 'echarts_timeseries_line',
slice_id: 2,
@@ -262,8 +264,10 @@ test('Recalculate charts in global filter scope when charts change', () => {
'3': {
id: 3,
form_data: {
slice_id: 3,
datasource: '3__table',
viz_type: 'echarts_timeseries_line',
color_scheme: 'supersetColors',
},
chartAlert: null,
chartStatus: 'rendered' as const,

View File

@@ -49,10 +49,8 @@ test('Return all chart ids in global scope with native filters', () => {
} as unknown as Filter,
};
const result = getRelatedCharts(filters, null, slices);
expect(result).toEqual({
filterKey1: [1, 2, 3],
});
const result = getRelatedCharts('filterKey1', filters.filterKey1, slices);
expect(result).toEqual([1, 2, 3]);
});
test('Return only chart ids in specific scope with native filters', () => {
@@ -74,10 +72,8 @@ test('Return only chart ids in specific scope with native filters', () => {
} as unknown as Filter,
};
const result = getRelatedCharts(filters, null, slices);
expect(result).toEqual({
filterKey1: [1, 3],
});
const result = getRelatedCharts('filterKey1', filters.filterKey1, slices);
expect(result).toEqual([1, 3]);
});
test('Return all chart ids with cross filter in global scope', () => {
@@ -90,10 +86,8 @@ test('Return all chart ids with cross filter in global scope', () => {
} as AppliedCrossFilterType,
};
const result = getRelatedCharts(filters, null, slices);
expect(result).toEqual({
'3': [1, 2],
});
const result = getRelatedCharts('3', filters['3'], slices);
expect(result).toEqual([1, 2]);
});
test('Return only chart ids in specific scope with cross filter', () => {
@@ -108,8 +102,6 @@ test('Return only chart ids in specific scope with cross filter', () => {
} as AppliedCrossFilterType,
};
const result = getRelatedCharts(filters, null, slices);
expect(result).toEqual({
'1': [2],
});
const result = getRelatedCharts('1', filters['1'], slices);
expect(result).toEqual([2]);
});

View File

@@ -32,25 +32,25 @@ function isGlobalScope(scope: number[], slices: Record<string, Slice>) {
}
function getRelatedChartsForSelectFilter(
nativeFilter: AppliedNativeFilterType | Filter,
slices: Record<string, Slice>,
chartsInScope: number[],
) {
return Object.values(slices)
.filter(slice => {
const { slice_id } = slice;
// all have been selected, always apply
if (isGlobalScope(chartsInScope, slices)) return true;
// hand-picked in scope, always apply
if (chartsInScope.includes(slice_id)) return true;
): number[] {
// all have been selected, always apply
if (isGlobalScope(chartsInScope, slices)) {
return Object.keys(slices).map(Number);
}
return false;
})
.map(slice => slice.slice_id);
const chartsInScopeSet = new Set(chartsInScope);
return Object.values(slices).reduce((result: number[], slice) => {
if (chartsInScopeSet.has(slice.slice_id)) {
result.push(slice.slice_id);
}
return result;
}, []);
}
function getRelatedChartsForCrossFilter(
filterKey: string,
crossFilter: AppliedCrossFilterType,
slices: Record<string, Slice>,
scope: number[],
): number[] {
@@ -58,86 +58,56 @@ function getRelatedChartsForCrossFilter(
if (!sourceSlice) return [];
return Object.values(slices)
.filter(slice => {
// cross-filter emitter
if (slice.slice_id === Number(filterKey)) return false;
// hand-picked in the scope, always apply
const fullScope = [
...scope.filter(s => String(s) !== filterKey),
Number(filterKey),
];
if (isGlobalScope(fullScope, slices)) return true;
// hand-picked in the scope, always apply
if (scope.includes(slice.slice_id)) return true;
const fullScope = [
...scope.filter(s => String(s) !== filterKey),
Number(filterKey),
];
const scopeSet = new Set(scope);
return false;
})
.map(slice => slice.slice_id);
return Object.values(slices).reduce((result: number[], slice) => {
if (slice.slice_id === Number(filterKey)) {
return result;
}
// Check if it's in the global scope
if (isGlobalScope(fullScope, slices)) {
result.push(slice.slice_id);
return result;
}
// Check if it's hand-picked in scope
if (scopeSet.has(slice.slice_id)) {
result.push(slice.slice_id);
}
return result;
}, []);
}
export function getRelatedCharts(
filters: Record<
string,
AppliedNativeFilterType | AppliedCrossFilterType | Filter
>,
checkFilters: Record<
string,
AppliedNativeFilterType | AppliedCrossFilterType | Filter
> | null,
filterKey: string,
filter: AppliedNativeFilterType | AppliedCrossFilterType | Filter,
slices: Record<string, Slice>,
) {
const related = Object.entries(filters).reduce((acc, [filterKey, filter]) => {
const isCrossFilter =
Object.keys(slices).includes(filterKey) &&
isAppliedCrossFilterType(filter);
let related: number[] = [];
const isCrossFilter =
Object.keys(slices).includes(filterKey) && isAppliedCrossFilterType(filter);
const chartsInScope = Array.isArray(filter.scope)
? filter.scope
: (filter as Filter).chartsInScope ?? [];
const chartsInScope = Array.isArray(filter.scope)
? filter.scope
: (filter as Filter).chartsInScope ?? [];
if (isCrossFilter) {
const checkFilter = checkFilters?.[filterKey] as AppliedCrossFilterType;
const crossFilter = filter as AppliedCrossFilterType;
const wasRemoved = !!(
((filter.values && filter.values.filters === undefined) ||
filter.values?.filters?.length === 0) &&
checkFilter?.values?.filters?.length
);
const actualCrossFilter = wasRemoved ? checkFilter : crossFilter;
if (isCrossFilter) {
related = getRelatedChartsForCrossFilter(filterKey, slices, chartsInScope);
}
return {
...acc,
[filterKey]: getRelatedChartsForCrossFilter(
filterKey,
actualCrossFilter,
slices,
chartsInScope,
),
};
}
const nativeFilter = filter as AppliedNativeFilterType | Filter;
// on highlight, a standard native filter is passed
// on apply, an applied native filter is passed
if (
isAppliedNativeFilterType(nativeFilter) ||
isNativeFilter(nativeFilter)
) {
return {
...acc,
[filterKey]: getRelatedChartsForSelectFilter(
nativeFilter,
slices,
chartsInScope,
),
};
}
return {
...acc,
[filterKey]: chartsInScope,
};
}, {});
const nativeFilter = filter as AppliedNativeFilterType | Filter;
// on highlight, a standard native filter is passed
// on apply, an applied native filter is passed
if (
!isCrossFilter ||
isAppliedNativeFilterType(nativeFilter) ||
isNativeFilter(nativeFilter)
) {
related = getRelatedChartsForSelectFilter(slices, chartsInScope);
}
return related;
}

View File

@@ -61,9 +61,7 @@ describe('useFilterFocusHighlightStyles', () => {
});
it('should return unfocused styles if chart is not in scope of focused native filter', async () => {
mockGetRelatedCharts.mockReturnValue({
'test-filter': [],
});
mockGetRelatedCharts.mockReturnValue([]);
const store = createMockStore({
nativeFilters: {
focusedFilterId: 'test-filter',
@@ -83,9 +81,7 @@ describe('useFilterFocusHighlightStyles', () => {
});
it('should return unfocused styles if chart is not in scope of hovered native filter', async () => {
mockGetRelatedCharts.mockReturnValue({
'test-filter': [],
});
mockGetRelatedCharts.mockReturnValue([]);
const store = createMockStore({
nativeFilters: {
hoveredFilterId: 'test-filter',
@@ -106,9 +102,7 @@ describe('useFilterFocusHighlightStyles', () => {
it('should return focused styles if chart is in scope of focused native filter', async () => {
const chartId = 18;
mockGetRelatedCharts.mockReturnValue({
testFilter: [chartId],
});
mockGetRelatedCharts.mockReturnValue([chartId]);
const store = createMockStore({
nativeFilters: {
focusedFilterId: 'testFilter',
@@ -129,9 +123,7 @@ describe('useFilterFocusHighlightStyles', () => {
it('should return focused styles if chart is in scope of hovered native filter', async () => {
const chartId = 18;
mockGetRelatedCharts.mockReturnValue({
testFilter: [chartId],
});
mockGetRelatedCharts.mockReturnValue([chartId]);
const store = createMockStore({
nativeFilters: {
hoveredFilterId: 'testFilter',
@@ -152,9 +144,7 @@ describe('useFilterFocusHighlightStyles', () => {
it('should return unfocused styles if focusedFilterField is targeting a different chart', async () => {
const chartId = 18;
mockGetRelatedCharts.mockReturnValue({
testFilter: [],
});
mockGetRelatedCharts.mockReturnValue([]);
const store = createMockStore({
dashboardState: {
focusedFilterField: {
@@ -178,9 +168,7 @@ describe('useFilterFocusHighlightStyles', () => {
it('should return focused styles if focusedFilterField chart equals our own', async () => {
const chartId = 18;
mockGetRelatedCharts.mockReturnValue({
testFilter: [chartId],
});
mockGetRelatedCharts.mockReturnValue([chartId]);
const store = createMockStore({
dashboardState: {
focusedFilterField: {

View File

@@ -54,18 +54,19 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
const slices =
useSelector((state: RootState) => state.sliceEntities.slices) || {};
const relatedCharts = getRelatedCharts(
nativeFilters.filters as Record<string, Filter>,
null,
slices,
);
const highlightedFilterId =
nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
if (!(focusedFilterScope || highlightedFilterId)) {
return {};
}
const relatedCharts = getRelatedCharts(
highlightedFilterId as string,
nativeFilters.filters[highlightedFilterId as string] as Filter,
slices,
);
// we use local styles here instead of a conditionally-applied class,
// because adding any conditional class to this container
// causes performance issues in Chrome.
@@ -80,7 +81,7 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
};
if (highlightedFilterId) {
if (relatedCharts[highlightedFilterId]?.includes(chartId)) {
if (relatedCharts.includes(chartId)) {
return focusedChartStyles;
}
} else if (

View File

@@ -80,7 +80,9 @@ function showFailureMessage(message: string) {
if (!window.parent || window.parent === window) {
showFailureMessage(
'This page is intended to be embedded in an iframe, but it looks like that is not the case.',
t(
'This page is intended to be embedded in an iframe, but it looks like that is not the case.',
),
);
}
@@ -141,7 +143,9 @@ function start() {
// something is most likely wrong with the guest token
logging.error(err);
showFailureMessage(
'Something went wrong with embedded authentication. Check the dev console for details.',
t(
'Something went wrong with embedded authentication. Check the dev console for details.',
),
);
},
);

View File

@@ -39,6 +39,7 @@ const propTypes = {
canOverwrite: PropTypes.bool.isRequired,
canDownload: PropTypes.bool.isRequired,
dashboardId: PropTypes.number,
colorScheme: PropTypes.string,
isStarred: PropTypes.bool.isRequired,
slice: PropTypes.object,
sliceName: PropTypes.string,
@@ -68,6 +69,7 @@ const additionalItemsStyles = theme => css`
export const ExploreChartHeader = ({
dashboardId,
colorScheme: dashboardColorScheme,
slice,
actions,
formData,
@@ -84,17 +86,15 @@ export const ExploreChartHeader = ({
const dispatch = useDispatch();
const { latestQueryFormData, sliceFormData } = chart;
const [isPropertiesModalOpen, setIsPropertiesModalOpen] = useState(false);
const updateCategoricalNamespace = async () => {
const { dashboards } = metadata || {};
const dashboard =
dashboardId && dashboards && dashboards.find(d => d.id === dashboardId);
if (!dashboard) {
if (!dashboard || !dashboardColorScheme) {
// clean up color namespace and shared color maps
// to avoid colors spill outside of dashboard context
resetColors(metadata?.color_namespace);
return;
}
if (dashboard) {
@@ -108,6 +108,7 @@ export const ExploreChartHeader = ({
// setting the chart to use the dashboard custom label colors if any
const dashboardMetadata = JSON.parse(result.json_metadata);
// ensure consistency with the dashboard
applyColors(dashboardMetadata);
} catch (error) {
logging.info(t('Unable to retrieve dashboard colors'));

View File

@@ -76,6 +76,9 @@ const propTypes = {
actions: PropTypes.object.isRequired,
datasource_type: PropTypes.string.isRequired,
dashboardId: PropTypes.number,
colorScheme: PropTypes.string,
ownColorScheme: PropTypes.string,
dashboardColorScheme: PropTypes.string,
isDatasourceMetaLoading: PropTypes.bool.isRequired,
chart: chartPropShape.isRequired,
slice: PropTypes.object,
@@ -563,6 +566,7 @@ function ExploreViewContainer(props) {
canOverwrite={props.can_overwrite}
canDownload={props.can_download}
dashboardId={props.dashboardId}
colorScheme={props.dashboardColorScheme}
isStarred={props.isStarred}
slice={props.slice}
sliceName={props.sliceName}
@@ -741,6 +745,9 @@ function mapStateToProps(state) {
},
);
const chart = charts[slice_id];
const colorScheme = explore.form_data?.color_scheme;
const ownColorScheme = explore.form_data?.own_color_scheme;
const dashboardColorScheme = explore.form_data?.dashboard_color_scheme;
let dashboardId = Number(explore.form_data?.dashboardId);
if (Number.isNaN(dashboardId)) {
@@ -753,6 +760,9 @@ function mapStateToProps(state) {
datasource_type: datasource.type,
datasourceId: datasource.datasource_id,
dashboardId,
colorScheme,
ownColorScheme,
dashboardColorScheme,
controls: explore.controls,
can_add: !!explore.can_add,
can_download: !!explore.can_download,

View File

@@ -29,6 +29,7 @@ import ColorSchemeControl, { ColorSchemes } from '.';
const defaultProps = () => ({
hasCustomLabelsColor: false,
sharedLabelsColors: [],
label: 'Color scheme',
labelMargin: 0,
name: 'color',
@@ -128,8 +129,8 @@ test('displays color scheme options', async () => {
});
});
test('Renders control with dashboard id', () => {
setup({ dashboardId: 1 });
test('Renders control with dashboard id and dashboard color scheme', () => {
setup({ dashboardId: 1, hasDashboardColorScheme: true });
expect(screen.getByText('Dashboard scheme')).toBeInTheDocument();
expect(
screen.getByLabelText('Select color scheme', { selector: 'input' }),

View File

@@ -26,6 +26,8 @@ import {
styled,
t,
useTheme,
getLabelsColorMap,
CategoricalColorNamespace,
} from '@superset-ui/core';
import AntdSelect from 'antd/lib/select';
import { isFunction, sortBy } from 'lodash';
@@ -35,6 +37,7 @@ import Icons from 'src/components/Icons';
import { SelectOptionsType } from 'src/components/Select/types';
import { StyledSelect } from 'src/components/Select/styles';
import { handleFilterOptionHelper } from 'src/components/Select/utils';
import { getColorNamespace } from 'src/utils/colorScheme';
import ColorSchemeLabel from './ColorSchemeLabel';
const { Option, OptGroup } = AntdSelect;
@@ -47,6 +50,12 @@ export interface ColorSchemes {
export interface ColorSchemeControlProps {
hasCustomLabelsColor: boolean;
hasDashboardColorScheme?: boolean;
hasSharedLabelsColor?: boolean;
sharedLabelsColors?: string[];
mapLabelsColors?: Record<string, any>;
colorNamespace?: string;
chartId?: number;
dashboardId?: number;
label: string;
name: string;
@@ -64,8 +73,8 @@ const StyledAlert = styled(Icons.AlertSolid)`
`;
const CUSTOM_LABEL_ALERT = t(
`This color scheme is being overridden by custom label colors.
Check the JSON metadata in the Advanced settings`,
`The colors of this chart might be overridden by custom label colors of the related dashboard.
Check the JSON metadata in the Advanced settings.`,
);
const DASHBOARD_ALERT = t(
@@ -73,18 +82,38 @@ const DASHBOARD_ALERT = t(
Edit the color scheme in the dashboard properties.`,
);
const DASHBOARD_CONTEXT_ALERT = t(
`You are viewing this chart in a dashboard context with labels shared across multiple charts.
The color scheme selection is disabled.`,
);
const DASHBOARD_CONTEXT_TOOLTIP = t(
`You are viewing this chart in the context of a dashboard that is directly affecting its colors.
To edit the color scheme, open this chart outside of the dashboard.`,
);
const Label = ({
label,
hasCustomLabelsColor,
dashboardId,
hasSharedLabelsColor,
hasCustomLabelsColor,
hasDashboardColorScheme,
}: Pick<
ColorSchemeControlProps,
'label' | 'hasCustomLabelsColor' | 'dashboardId'
| 'label'
| 'dashboardId'
| 'hasCustomLabelsColor'
| 'hasSharedLabelsColor'
| 'hasDashboardColorScheme'
>) => {
if (hasCustomLabelsColor || dashboardId) {
const alertTitle = hasCustomLabelsColor
? CUSTOM_LABEL_ALERT
: DASHBOARD_ALERT;
if (hasSharedLabelsColor || hasCustomLabelsColor || hasDashboardColorScheme) {
const alertTitle =
hasCustomLabelsColor && !hasSharedLabelsColor
? CUSTOM_LABEL_ALERT
: dashboardId && hasDashboardColorScheme
? DASHBOARD_ALERT
: DASHBOARD_CONTEXT_ALERT;
return (
<>
{label}{' '}
@@ -99,7 +128,12 @@ const Label = ({
const ColorSchemeControl = ({
hasCustomLabelsColor = false,
hasDashboardColorScheme = false,
mapLabelsColors = {},
sharedLabelsColors = [],
dashboardId,
colorNamespace,
chartId,
label = t('Color scheme'),
onChange = () => {},
value,
@@ -110,9 +144,21 @@ const ColorSchemeControl = ({
isLinear,
...rest
}: ColorSchemeControlProps) => {
const countSharedLabelsColor = sharedLabelsColors.length;
const colorMapInstance = getLabelsColorMap();
const chartLabels = chartId
? colorMapInstance.chartsLabelsMap.get(chartId)?.labels || []
: [];
const hasSharedLabelsColor = !!(
dashboardId &&
countSharedLabelsColor > 0 &&
chartLabels.some(label => sharedLabelsColors.includes(label))
);
const hasDashboardScheme = dashboardId && hasDashboardColorScheme;
const showDashboardLockedOption = hasDashboardScheme || hasSharedLabelsColor;
const theme = useTheme();
const currentScheme = useMemo(() => {
if (dashboardId) {
if (showDashboardLockedOption) {
return 'dashboard';
}
let result = value || defaultScheme;
@@ -121,13 +167,15 @@ const ColorSchemeControl = ({
result = schemesObject?.SUPERSET_DEFAULT?.id;
}
return result;
}, [dashboardId, defaultScheme, schemes, value]);
}, [defaultScheme, schemes, showDashboardLockedOption, value]);
const options = useMemo(() => {
if (dashboardId) {
if (showDashboardLockedOption) {
return [
<Option value="dashboard" label={t('dashboard')} key="dashboard">
<Tooltip title={DASHBOARD_ALERT}>{t('Dashboard scheme')}</Tooltip>
<Option value="dashboard" label={t('Dashboard')} key="dashboard">
<Tooltip title={DASHBOARD_CONTEXT_TOOLTIP}>
{t('Dashboard scheme')}
</Tooltip>
</Option>,
];
}
@@ -218,11 +266,29 @@ const ColorSchemeControl = ({
))}
</OptGroup>
));
}, [choices, dashboardId, isLinear, schemes]);
}, [choices, hasDashboardScheme, hasSharedLabelsColor, isLinear, schemes]);
// We can't pass on change directly because it receives a second
// parameter and it would be interpreted as the error parameter
const handleOnChange = (value: string) => onChange(value);
const handleOnChange = (value: string) => {
if (chartId) {
colorMapInstance.setOwnColorScheme(chartId, value);
if (dashboardId) {
const colorNameSpace = getColorNamespace(colorNamespace);
const categoricalNamespace =
CategoricalColorNamespace.getNamespace(colorNameSpace);
const sharedLabelsSet = new Set(sharedLabelsColors);
// reset colors except shared and custom labels to keep dashboard consistency
const resettableLabels = Object.keys(mapLabelsColors).filter(
l => !sharedLabelsSet.has(l),
);
categoricalNamespace.resetColorsForLabels(resettableLabels);
}
}
onChange(value);
};
return (
<>
@@ -231,8 +297,10 @@ const ColorSchemeControl = ({
label={
<Label
label={label}
hasCustomLabelsColor={hasCustomLabelsColor}
dashboardId={dashboardId}
hasCustomLabelsColor={hasCustomLabelsColor}
hasDashboardColorScheme={hasDashboardColorScheme}
hasSharedLabelsColor={hasSharedLabelsColor}
/>
}
/>
@@ -249,7 +317,7 @@ const ColorSchemeControl = ({
`}
aria-label={t('Select color scheme')}
allowClear={clearable}
disabled={!!dashboardId}
disabled={hasDashboardScheme || hasSharedLabelsColor}
onChange={handleOnChange}
placeholder={t('Select scheme')}
value={currentScheme}

View File

@@ -16,13 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import sinon from 'sinon';
import { shallow } from 'enzyme';
import { render, screen, fireEvent } from 'spec/helpers/testing-library';
import { AGGREGATES } from 'src/explore/constants';
import AdhocMetricOption from 'src/explore/components/controls/MetricControl/AdhocMetricOption';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
import ControlPopover from '../ControlPopover/ControlPopover';
import userEvent from '@testing-library/user-event';
const columns = [
{ type: 'VARCHAR(255)', column_name: 'source' },
@@ -35,49 +33,73 @@ const sumValueAdhocMetric = new AdhocMetric({
aggregate: AGGREGATES.SUM,
});
const datasource = {
type: 'table',
id: 1,
uid: '1__table',
columnFormats: {},
verboseMap: {},
};
const defaultProps = {
adhocMetric: sumValueAdhocMetric,
savedMetric: {},
savedMetricsOptions: [],
onMetricEdit: jest.fn(),
columns,
datasource,
onMoveLabel: jest.fn(),
onDropLabel: jest.fn(),
index: 0,
};
function setup(overrides) {
const onMetricEdit = sinon.spy();
const props = {
adhocMetric: sumValueAdhocMetric,
savedMetric: {},
savedMetrics: [],
onMetricEdit,
columns,
onMoveLabel: () => {},
onDropLabel: () => {},
index: 0,
...defaultProps,
...overrides,
};
const wrapper = shallow(<AdhocMetricOption {...props} />)
.find('AdhocMetricPopoverTrigger')
.shallow();
return { wrapper, onMetricEdit };
return render(<AdhocMetricOption {...props} />, { useDnd: true });
}
describe('AdhocMetricOption', () => {
it('renders an overlay trigger wrapper for the label', () => {
const { wrapper } = setup();
expect(wrapper.find(ControlPopover)).toExist();
expect(wrapper.find('OptionControlLabel')).toExist();
});
it('overwrites the adhocMetric in state with onLabelChange', () => {
const { wrapper } = setup();
wrapper.instance().onLabelChange({ target: { value: 'new label' } });
expect(wrapper.state('title').label).toBe('new label');
expect(wrapper.state('title').hasCustomLabel).toBe(true);
});
it('returns to default labels when the custom label is cleared', () => {
const { wrapper } = setup();
expect(wrapper.state('title').label).toBe('SUM(value)');
wrapper.instance().onLabelChange({ target: { value: 'new label' } });
expect(wrapper.state('title').label).toBe('new label');
wrapper.instance().onLabelChange({ target: { value: '' } });
expect(wrapper.state('title').label).toBe('SUM(value)');
expect(wrapper.state('title').hasCustomLabel).toBe(false);
});
test('renders an overlay trigger wrapper for the label', () => {
setup();
expect(screen.getByText('SUM(value)')).toBeInTheDocument();
});
test('overwrites the adhocMetric in state with onLabelChange', async () => {
setup();
userEvent.click(screen.getByText('SUM(value)'));
userEvent.click(screen.getByTestId(/AdhocMetricEditTitle#trigger/i));
const labelInput = await screen.findByTestId(/AdhocMetricEditTitle#input/i);
userEvent.clear(labelInput);
userEvent.type(labelInput, 'new label');
expect(labelInput).toHaveValue('new label');
fireEvent.keyPress(labelInput, {
key: 'Enter',
charCode: 13,
});
expect(screen.getByText(/new label/i)).toBeInTheDocument();
});
test('returns to default labels when the custom label is cleared', async () => {
setup();
userEvent.click(screen.getByText('SUM(value)'));
userEvent.click(screen.getByTestId(/AdhocMetricEditTitle#trigger/i));
const labelInput = await screen.findByTestId(/AdhocMetricEditTitle#input/i);
userEvent.clear(labelInput);
userEvent.type(labelInput, 'new label');
fireEvent.keyPress(labelInput, {
key: 'Enter',
charCode: 13,
});
expect(labelInput).not.toBeInTheDocument();
expect(screen.getByText(/new label/i)).toBeInTheDocument();
userEvent.click(screen.getByTestId(/AdhocMetricEditTitle#trigger/i));
expect(screen.getByPlaceholderText(/new label/i)).toBeInTheDocument();
userEvent.clear(labelInput);
fireEvent.keyPress(labelInput, {
key: 'Enter',
charCode: 13,
});
expect(screen.getByPlaceholderText('SUM(value)')).toBeInTheDocument();
});

View File

@@ -16,38 +16,40 @@
* specific language governing permissions and limitations
* under the License.
*/
import { shallow } from 'enzyme';
import { AGGREGATES } from 'src/explore/constants';
import { render, screen } from 'spec/helpers/testing-library';
import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue';
import AdhocMetricOption from 'src/explore/components/controls/MetricControl/AdhocMetricOption';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
import { AGGREGATES } from 'src/explore/constants';
const sumValueAdhocMetric = new AdhocMetric({
column: { type: 'DOUBLE', column_name: 'value' },
aggregate: AGGREGATES.SUM,
});
describe('MetricDefinitionValue', () => {
it('renders a MetricOption given a saved metric', () => {
const wrapper = shallow(
<MetricDefinitionValue
onMetricEdit={() => {}}
option={{ metric_name: 'a_saved_metric', expression: 'COUNT(*)' }}
index={1}
/>,
);
expect(wrapper.find('AdhocMetricOption')).toExist();
});
const setup = propOverrides => {
const props = {
onMetricEdit: jest.fn(),
option: sumValueAdhocMetric,
index: 1,
columns: [],
savedMetrics: [],
savedMetricsOptions: [],
datasource: {},
onMoveLabel: jest.fn(),
onDropLabel: jest.fn(),
...propOverrides,
};
return render(<MetricDefinitionValue {...props} />, { useDnd: true });
};
it('renders an AdhocMetricOption given an adhoc metric', () => {
const wrapper = shallow(
<MetricDefinitionValue
onMetricEdit={() => {}}
option={sumValueAdhocMetric}
index={1}
/>,
);
expect(wrapper.find(AdhocMetricOption)).toExist();
test('renders a MetricOption given a saved metric', () => {
setup({
option: { metric_name: 'a_saved_metric', expression: 'COUNT(*)' },
});
expect(screen.getByText('a_saved_metric')).toBeInTheDocument();
});
test('renders an AdhocMetricOption given an adhoc metric', () => {
setup();
expect(screen.getByText('SUM(value)')).toBeInTheDocument();
});

View File

@@ -24,6 +24,7 @@ import {
import { JsonArray, JsonValue, t } from '@superset-ui/core';
import { ControlProps } from 'src/explore/components/Control';
import builtInControlComponents from 'src/explore/components/controls';
import useEffectEvent from 'src/hooks/useEffectEvent';
/**
* Full control component map.
@@ -72,7 +73,7 @@ export type AsyncVerify = (
* Whether the extra props will update the original props.
*/
function hasUpdates(
props: ControlPropsWithExtras,
props: Partial<ControlPropsWithExtras>,
newProps: ExtraControlProps,
) {
return (
@@ -165,17 +166,17 @@ export default function withAsyncVerification({
[basicOnChange, otherProps, verifiedProps],
);
useEffect(() => {
if (needAsyncVerification && verify) {
const verifyProps = useEffectEvent(
(verifyFunc: AsyncVerify, props: typeof otherProps) => {
if (showLoadingState) {
setIsLoading(true);
}
verify(otherProps)
verifyFunc(props)
.then(updatedProps => {
if (showLoadingState) {
setIsLoading(false);
}
if (updatedProps && hasUpdates(otherProps, updatedProps)) {
if (updatedProps && hasUpdates(verifiedProps, updatedProps)) {
setVerifiedProps({
// save isLoading in combination with other props to avoid
// rendering twice.
@@ -198,14 +199,14 @@ export default function withAsyncVerification({
);
}
});
},
);
useEffect(() => {
if (needAsyncVerification && verify) {
verifyProps(verify, otherProps);
}
}, [
needAsyncVerification,
showLoadingState,
verify,
otherProps,
addWarningToast,
]);
}, [needAsyncVerification, verify, otherProps, verifyProps]);
return (
<ControlComponent

View File

@@ -61,6 +61,9 @@ describe('controlUtils', () => {
controls: {},
form_data: { datasource: '1__table', viz_type: 'table' },
common: {},
slice: {
slice_id: 1,
},
};
beforeAll(() => {

View File

@@ -151,10 +151,9 @@ const getExpectedResultFormData = (overrides: JsonObject = {}) => ({
girl: '#FF69B4',
boy: '#ADD8E6',
},
shared_label_colors: {
boy: '#ADD8E6',
girl: '#FF69B4',
},
shared_label_colors: ['boy', 'girl'],
own_color_scheme: 'supersetColors',
dashboard_color_scheme: 'd3Category20b',
extra_filters: [
{
col: '__time_range',

View File

@@ -218,11 +218,18 @@ export const getFormDataWithDashboardContext = (
{},
);
const ownColorScheme = exploreFormData.color_scheme;
const dashboardColorScheme = dashboardContextFormData.color_scheme;
const appliedColorScheme = dashboardColorScheme || ownColorScheme;
return {
...exploreFormData,
...dashboardContextFormData,
...filterBoxData,
...nativeFiltersData,
...adhocFilters,
own_color_scheme: ownColorScheme,
color_scheme: appliedColorScheme,
dashboard_color_scheme: dashboardColorScheme,
};
};

Some files were not shown because too many files have changed in this diff Show More