mirror of
https://github.com/apache/superset.git
synced 2026-07-07 15:25:36 +00:00
Compare commits
27 Commits
4.1.0
...
fix_docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
774910f40d | ||
|
|
dbbcc11a98 | ||
|
|
cebd45778f | ||
|
|
1b2ecc6955 | ||
|
|
1f5e567645 | ||
|
|
c467fb566d | ||
|
|
cc0ed0fef4 | ||
|
|
4f463399a7 | ||
|
|
6264ff5165 | ||
|
|
1410e528a4 | ||
|
|
f704b0f556 | ||
|
|
bdfd5cd4ec | ||
|
|
af44b14fbe | ||
|
|
29c76ef1d5 | ||
|
|
7953c89d51 | ||
|
|
fd4c3dce44 | ||
|
|
234f8c94d1 | ||
|
|
bc2e51d8d0 | ||
|
|
2787167abe | ||
|
|
9e84e13888 | ||
|
|
cfd24e3ccd | ||
|
|
aaecec2e03 | ||
|
|
66fe0b0594 | ||
|
|
0d0b43062e | ||
|
|
72df46a729 | ||
|
|
f7cfd9182a | ||
|
|
5faaaf978b |
10
.github/workflows/superset-docs-verify.yml
vendored
10
.github/workflows/superset-docs-verify.yml
vendored
@@ -12,6 +12,16 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
linkinator:
|
||||
name: Link Checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: JustinBeckwith/linkinator-action@v1.10.4
|
||||
with:
|
||||
paths: "**/*.md, **/*.mdx"
|
||||
linksToSkip: '^https://github.com/apache/(superset|incubator-superset)/(pull|issue)/\d+, http://localhost:8088/, docker/.env-non-dev, http://127.0.0.1:3000/, http://localhost:9001/, https://charts.bitnami.com/bitnami, https://www.li.me/, https://www.fanatics.com/, https://tails.com/gb/, https://www.techaudit.info/, https://avetilearning.com/, https://www.udemy.com/, https://trustmedis.com/, http://theiconic.com.au/, https://dev.mysql.com/doc/refman/5.7/en/innodb-limits.html, https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fembedded-sdk?style=flat, https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-pivot-table?style=flat, https://vkusvill.ru/'
|
||||
# verbosity: 'ERROR'
|
||||
build-deploy:
|
||||
name: Build & Deploy
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
50
CHANGELOG/4.1.1.md
Normal file
50
CHANGELOG/4.1.1.md
Normal file
@@ -0,0 +1,50 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
## Change Log
|
||||
|
||||
### 4.1 (Fri Nov 15 22:13:57 2024 +0530)
|
||||
|
||||
**Database Migrations**
|
||||
|
||||
**Features**
|
||||
|
||||
**Fixes**
|
||||
|
||||
- [#30886](https://github.com/apache/superset/pull/30886) fix: blocks UI elements on right side (@samarsrivastav)
|
||||
- [#30859](https://github.com/apache/superset/pull/30859) fix(package.json): Pin luxon version to unblock master (@geido)
|
||||
- [#30588](https://github.com/apache/superset/pull/30588) fix(explore): column data type tooltip format (@mistercrunch)
|
||||
- [#29911](https://github.com/apache/superset/pull/29911) fix: Rename database from 'couchbasedb' to 'couchbase' in documentation and db_engine_specs (@ayush-couchbase)
|
||||
- [#30828](https://github.com/apache/superset/pull/30828) fix(TimezoneSelector): Failing unit tests due to timezone change (@geido)
|
||||
- [#30875](https://github.com/apache/superset/pull/30875) fix: don't show metadata for embedded dashboards (@sadpandajoe)
|
||||
- [#30851](https://github.com/apache/superset/pull/30851) fix: Graph chart colors (@michael-s-molina)
|
||||
- [#29867](https://github.com/apache/superset/pull/29867) fix(capitalization): Capitalizing a button. (@rusackas)
|
||||
- [#29782](https://github.com/apache/superset/pull/29782) fix(translations): Translate embedded errors (@rusackas)
|
||||
- [#29772](https://github.com/apache/superset/pull/29772) fix: Fixing incomplete string escaping. (@rusackas)
|
||||
- [#29725](https://github.com/apache/superset/pull/29725) fix(frontend/docker, ci): fix borked Docker build due to Lerna v8 uplift (@hainenber)
|
||||
|
||||
**Others**
|
||||
|
||||
- [#30576](https://github.com/apache/superset/pull/30576) chore: add link to Superset when report error (@eschutho)
|
||||
- [#29786](https://github.com/apache/superset/pull/29786) refactor(Slider): Upgrade Slider to Antd 5 (@geido)
|
||||
- [#29674](https://github.com/apache/superset/pull/29674) refactor(ChartCreation): Migrate tests to RTL (@rtexelm)
|
||||
- [#29843](https://github.com/apache/superset/pull/29843) refactor(controls): Migrate AdhocMetricOption.test to RTL (@rtexelm)
|
||||
- [#29845](https://github.com/apache/superset/pull/29845) refactor(controls): Migrate MetricDefinitionValue.test to RTL (@rtexelm)
|
||||
- [#28424](https://github.com/apache/superset/pull/28424) docs: Check markdown files for bad links using linkinator (@rusackas)
|
||||
- [#29768](https://github.com/apache/superset/pull/29768) docs(contributing): fix broken link to translations sub-section (@sfirke)
|
||||
@@ -94,9 +94,9 @@ This statement thanks the following, on which it draws for content and inspirati
|
||||
|
||||
* [CouchDB Project Code of conduct](http://couchdb.apache.org/conduct.html)
|
||||
* [Fedora Project Code of Conduct](http://fedoraproject.org/code-of-conduct)
|
||||
* [Speak Up! Code of Conduct](http://speakup.io/coc.html)
|
||||
* [Speak Up! Code of Conduct](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html)
|
||||
* [Django Code of Conduct](https://www.djangoproject.com/conduct/)
|
||||
* [Debian Code of Conduct](http://www.debian.org/vote/2014/vote_002)
|
||||
* [Debian Code of Conduct](https://www.debian.org/vote/2014/vote_002)
|
||||
* [Twitter Open Source Code of Conduct](https://github.com/twitter/code-of-conduct/blob/master/code-of-conduct.md)
|
||||
* [Mozilla Code of Conduct/Draft](https://wiki.mozilla.org/Code_of_Conduct/Draft#Conflicts_of_Interest)
|
||||
* [Python Diversity Appendix](https://www.python.org/community/diversity/)
|
||||
|
||||
@@ -57,10 +57,11 @@ RUN npm run build-translation
|
||||
RUN rm /app/superset/translations/*/LC_MESSAGES/*.po
|
||||
RUN rm /app/superset/translations/messages.pot
|
||||
|
||||
FROM python:${PY_VER} AS python-base
|
||||
######################################################################
|
||||
# Final lean image...
|
||||
######################################################################
|
||||
FROM python:${PY_VER} AS lean
|
||||
FROM python-base AS lean
|
||||
|
||||
WORKDIR /app
|
||||
ENV LANG=C.UTF-8 \
|
||||
|
||||
@@ -505,7 +505,7 @@ We also need to update the Environment section of [ISSUE_TEMPLATE/bug-report.yml
|
||||
|
||||
Docker release with proper tags should happen automatically as version
|
||||
tags get pushed to the `apache/superset` GitHub repository through this
|
||||
[GitHub action](https://github.com/apache/superset/blob/master/.github/workflows/docker-release.yml)
|
||||
[GitHub action](https://github.com/apache/superset/blob/master/.github/workflows/docker.yml)
|
||||
|
||||
Note that this GH action implements a `workflow_dispatch` trigger,
|
||||
meaning that it can be triggered manually from the GitHub UI. If anything
|
||||
|
||||
@@ -102,7 +102,7 @@ Some of the new features in this release are disabled by default. Each has a fea
|
||||
This release includes **hundreds** of bugfixes and stability enhancements. Future major releases will have a continued emphasis on providing a stable and bug-free experience for the user.
|
||||
|
||||
# PR Highlights
|
||||
Below is a highlight of the PRs included in this update. The full list is much longer, and can be found [here](apache/incubator-superset/CHANGELOG.md).
|
||||
Below is a highlight of the PRs included in this update. The full list is much longer, and can be found [here](https://github.com/apache/superset/blob/master/CHANGELOG.md).
|
||||
|
||||
## User Experience
|
||||
- Revert "refactor: Remove usages of reactable from TimeTable (#11046)" (#[11150](https://github.com/apache/incubator-superset/pull/11150))
|
||||
@@ -222,4 +222,4 @@ Below is a highlight of the PRs included in this update. The full list is much l
|
||||
## Complete Changelog
|
||||
Backwards incompatible changes and can be found [here](../../UPDATING.md).
|
||||
|
||||
To see the complete changelog, see [apache/incubator-superset/CHANGELOG.md](https://github.com/apache/superset/blob/master/CHANGELOG.md)
|
||||
To see the complete changelog, see [apache/superset/CHANGELOG.md](https://github.com/apache/superset/blob/master/CHANGELOG.md)
|
||||
|
||||
@@ -137,6 +137,6 @@ when available.
|
||||
**Changelog**
|
||||
|
||||
To see the complete changelog in this release, head to
|
||||
[CHANGELOG.MD](https://github.com/apache/superset/blob/1.5/CHANGELOG/1.5.0.md).
|
||||
[CHANGELOG.MD](https://github.com/apache/superset/blob/master/CHANGELOG/1.5.0.md).
|
||||
As mentioned earlier, this release has a MASSIVE amount of bug fixes. The full
|
||||
changelog lists all of them!
|
||||
|
||||
@@ -34,7 +34,7 @@ We released a [Big Number with Time Period Comparison](https://github.com/apache
|
||||
</div>
|
||||
|
||||
### Table with Time Comparison
|
||||
Added functionality to do [table time comparisons](https://github.com/apache/superset/pull/28057) behind the `CHART_PLUGINS_EXPERIMENTAL` feature flag. This will help improve and facilitate efficient data analysis.
|
||||
Added functionality to do [table time comparisons](https://github.com/apache/superset/pull/28057). This will help improve and facilitate efficient data analysis.
|
||||
|
||||
<div>
|
||||
<image src="media/table_with_time.png" alt="Image" width="100%">
|
||||
|
||||
@@ -45,7 +45,7 @@ These features are **finished** but currently being tested. They are usable, but
|
||||
- CACHE_IMPERSONATION
|
||||
- CONFIRM_DASHBOARD_DIFF
|
||||
- DRILL_TO_DETAIL
|
||||
- DYNAMIC_PLUGINS: [(docs)](https://superset.apache.org/docs/configuration/running-on-kubernetes)
|
||||
- DYNAMIC_PLUGINS
|
||||
- ENABLE_SUPERSET_META_DB: [(docs)](https://superset.apache.org/docs/configuration/databases/#querying-across-databases)
|
||||
- ESTIMATE_QUERY_COST
|
||||
- GLOBAL_ASYNC_QUERIES [(docs)](https://github.com/apache/superset/blob/master/CONTRIBUTING.md#async-chart-queries)
|
||||
@@ -77,7 +77,7 @@ independently. This new framework will also allow for non-boolean configurations
|
||||
|
||||
- ALERTS_ATTACH_REPORTS
|
||||
- ALLOW_ADHOC_SUBQUERY
|
||||
- DASHBOARD_RBAC [(docs)](https://superset.apache.org/docs/using-superset/first-dashboard#manage-access-to-dashboards)
|
||||
- DASHBOARD_RBAC [(docs)](https://superset.apache.org/docs/using-superset/creating-your-first-dashboard#manage-access-to-dashboards)
|
||||
- DATAPANEL_CLOSED_BY_DEFAULT
|
||||
- DRUID_JOINS
|
||||
- EMBEDDABLE_CHARTS
|
||||
|
||||
@@ -25,16 +25,17 @@ all you have to do is file a simple PR [like this one](https://github.com/apache
|
||||
the categorization is inaccurate, please file a PR with your correction as well.
|
||||
Join our growing community!
|
||||
|
||||
|
||||
### Sharing Economy
|
||||
|
||||
- [Airbnb](https://github.com/airbnb)
|
||||
- [Faasos](http://faasos.com/) [@shashanksingh]
|
||||
- [Hostnfly](https://www.hostnfly.com/) [@alexisrosuel]
|
||||
- [Lime](https://www.limebike.com/) [@cxmcc]
|
||||
- [Lime](https://www.li.me/) [@cxmcc]
|
||||
- [Lyft](https://www.lyft.com/)
|
||||
- [Ontruck](https://www.ontruck.com/)
|
||||
|
||||
### Financial Services
|
||||
|
||||
- [Aktia Bank plc](https://www.aktia.com) [@villebro]
|
||||
- [American Express](https://www.americanexpress.com) [@TheLastSultan]
|
||||
- [Cape Crypto](https://capecrypto.com)
|
||||
@@ -47,15 +48,16 @@ Join our growing community!
|
||||
- [bumper](https://www.bumper.co/) [@vasu-ram, @JamiePercival]
|
||||
|
||||
### Gaming
|
||||
- [Digit Game Studios](https://www.digitgaming.com/)
|
||||
|
||||
- [Popoko VM Games Studio](https://popoko.live)
|
||||
|
||||
### E-Commerce
|
||||
|
||||
- [AiHello](https://www.aihello.com) [@ganeshkrishnan1]
|
||||
- [Bazaar Technologies](https://www.bazaartech.com) [@umair-abro]
|
||||
- [Dragonpass](https://www.dragonpass.com.cn/) [@zhxjdwh]
|
||||
- [Dropit Shopping](https://www.dropit.shop/) [@dropit-dev]
|
||||
- [Fanatics](https://www.fanatics.com) [@coderfender]
|
||||
- [Fanatics](https://www.fanatics.com/) [@coderfender]
|
||||
- [Fordeal](http://www.fordeal.com) [@Renkai]
|
||||
- [GFG - Global Fashion Group](https://global-fashion-group.com) [@ksaagariconic]
|
||||
- [HuiShouBao](http://www.huishoubao.com/) [@Yukinoshita-Yukino]
|
||||
@@ -64,14 +66,15 @@ Join our growing community!
|
||||
- [Rakuten Viki](https://www.viki.com)
|
||||
- [Shopee](https://shopee.sg) [@xiaohanyu]
|
||||
- [Shopkick](https://www.shopkick.com) [@LAlbertalli]
|
||||
- [Tails.com](https://tails.com) [@alanmcruickshank]
|
||||
- [Tails.com](https://tails.com/gb/) [@alanmcruickshank]
|
||||
- [THE ICONIC](http://theiconic.com.au/) [@ksaagariconic]
|
||||
- [Utair](https://www.utair.ru) [@utair-digital]
|
||||
- [VkusVill](https://www.vkusvill.ru) [@ETselikov]
|
||||
- [VkusVill](https://vkusvill.ru/) [@ETselikov]
|
||||
- [Zalando](https://www.zalando.com) [@dmigo]
|
||||
- [Zalora](https://www.zalora.com) [@ksaagariconic]
|
||||
|
||||
### Enterprise Technology
|
||||
|
||||
- [A3Data](https://a3data.com.br) [@neylsoncrepalde]
|
||||
- [Analytics Aura](https://analyticsaura.com/) [@Analytics-Aura]
|
||||
- [Apollo GraphQL](https://www.apollographql.com/) [@evans]
|
||||
@@ -80,6 +83,7 @@ Join our growing community!
|
||||
- [Caizin](https://caizin.com/) [@tejaskatariya]
|
||||
- [Careem](https://www.careem.com/) [@SamraHanifCareem]
|
||||
- [Cloudsmith](https://cloudsmith.io) [@alancarson]
|
||||
- [CnOvit](https://www.cnovit.com/) [@xieshaohu]
|
||||
- [Cyberhaven](https://www.cyberhaven.com/) [@toliver-ch]
|
||||
- [Deepomatic](https://deepomatic.com/) [@Zanoellia]
|
||||
- [Dial Once](https://www.dial-once.com/)
|
||||
@@ -88,8 +92,7 @@ Join our growing community!
|
||||
- [Endress+Hauser](http://www.endress.com/) [@rumbin]
|
||||
- [FBK - ICT center](http://ict.fbk.eu)
|
||||
- [Gavagai](https://gavagai.io) [@gavagai-corp]
|
||||
- [GfK Data Lab](http://datalab.gfk.com) [@mherr]
|
||||
- [GrowthSimple](https://growthsimple.ai/)
|
||||
- [GfK Data Lab](https://www.gfk.com/home) [@mherr]
|
||||
- [Hydrolix](https://www.hydrolix.io/)
|
||||
- [Intercom](https://www.intercom.com/) [@kate-gallo]
|
||||
- [jampp](https://jampp.com/)
|
||||
@@ -104,17 +107,14 @@ Join our growing community!
|
||||
- [Peak AI](https://www.peak.ai/) [@azhar22k]
|
||||
- [PeopleDoc](https://www.people-doc.com) [@rodo]
|
||||
- [Preset, Inc.](https://preset.io)
|
||||
- [Pronto Tools](http://www.prontotools.io) [@zkan]
|
||||
- [PubNub](https://pubnub.com) [@jzucker2]
|
||||
- [ReadyTech](https://www.readytech.io)
|
||||
- [Reward Gateway](https://www.rewardgateway.com)
|
||||
- [ScopeAI](https://www.getscopeai.com) [@iloveluce]
|
||||
- [Showmax](https://tech.showmax.com) [@bobek]
|
||||
- [source{d}](https://www.sourced.tech) [@marnovo]
|
||||
- [Steamroot](https://streamroot.io/)
|
||||
- [Showmax](https://showmax.com) [@bobek]
|
||||
- [TechAudit](https://www.techaudit.info) [@ETselikov]
|
||||
- [Tenable](https://www.tenable.com) [@dflionis]
|
||||
- [Tentacle](https://public.tentaclecmi.com) [@jdclarke5]
|
||||
- [Tentacle](https://tentaclecmi.com) [@jdclarke5]
|
||||
- [timbr.ai](https://timbr.ai/) [@semantiDan]
|
||||
- [Tobii](http://www.tobii.com/) [@dwa]
|
||||
- [Tooploox](https://www.tooploox.com/) [@jakubczaplicki]
|
||||
@@ -124,6 +124,7 @@ Join our growing community!
|
||||
- [Zeta](https://www.zeta.tech/) [@shaikidris]
|
||||
|
||||
### Media & Entertainment
|
||||
|
||||
- [6play](https://www.6play.fr) [@CoryChaplin]
|
||||
- [bilibili](https://www.bilibili.com) [@Moinheart]
|
||||
- [BurdaForward](https://www.burda-forward.de/en/)
|
||||
@@ -136,6 +137,7 @@ Join our growing community!
|
||||
- [Zaihang](http://www.zaih.com/)
|
||||
|
||||
### Education
|
||||
|
||||
- [Aveti Learning](https://avetilearning.com/) [@TheShubhendra]
|
||||
- [Brilliant.org](https://brilliant.org/)
|
||||
- [Platzi.com](https://platzi.com/)
|
||||
@@ -146,6 +148,7 @@ Join our growing community!
|
||||
- [WikiMedia Foundation](https://wikimediafoundation.org) [@vg]
|
||||
|
||||
### Energy
|
||||
|
||||
- [Airboxlab](https://foobot.io) [@antoine-galataud]
|
||||
- [DouroECI](https://www.douroeci.com/) [@nunohelibeires]
|
||||
- [Safaricom](https://www.safaricom.co.ke/) [@mmutiso]
|
||||
@@ -153,31 +156,35 @@ Join our growing community!
|
||||
- [Wattbewerb](https://wattbewerb.de/) [@wattbewerb]
|
||||
|
||||
### Healthcare
|
||||
|
||||
- [Amino](https://amino.com) [@shkr]
|
||||
- [Beans](https://www.beans.fi) [@kakoni]
|
||||
- [Bluesquare](https://www.bluesquarehub.com/) [@madewulf]
|
||||
- [Care](https://www.getcare.io/)[@alandao2021]
|
||||
- [Living Goods](https://www.livinggoods.org) [@chelule]
|
||||
- [Maieutical Labs](https://maieuticallabs.it) [@xrmx]
|
||||
- [QPID Health](http://www.qpidhealth.com/)
|
||||
- [REDCap Cloud](https://www.redcapcloud.com/)
|
||||
- [TrustMedis](https://trustmedis.com) [@famasya]
|
||||
- [TrustMedis](https://trustmedis.com/) [@famasya]
|
||||
- [WeSure](https://www.wesure.cn/)
|
||||
|
||||
### HR / Staffing
|
||||
|
||||
- [Swile](https://www.swile.co/) [@PaoloTerzi]
|
||||
- [Symmetrics](https://www.symmetrics.fyi)
|
||||
- [bluquist](https://bluquist.com/)
|
||||
|
||||
### Government
|
||||
|
||||
- [City of Ann Arbor, MI](https://www.a2gov.org/) [@sfirke]
|
||||
- [RIS3 Strategy of CZ, MIT CR](https://www.ris3.cz/) [@RIS3CZ]
|
||||
|
||||
### Travel
|
||||
|
||||
- [Agoda](https://www.agoda.com/) [@lostseaway, @maiake, @obombayo]
|
||||
- [Skyscanner](https://www.skyscanner.net/) [@cleslie, @stanhoucke]
|
||||
|
||||
### Others
|
||||
|
||||
- [10Web](https://10web.io/)
|
||||
- [AI inside](https://inside.ai/en/)
|
||||
- [Automattic](https://automattic.com/) [@Khrol, @Usiel]
|
||||
|
||||
@@ -228,7 +228,8 @@ assists people when migrating to a new version.
|
||||
- [19273](https://github.com/apache/superset/pull/19273): The `SUPERSET_CELERY_WORKERS` and `SUPERSET_WORKERS` config keys has been removed. Configure Celery directly using `CELERY_CONFIG` on Superset.
|
||||
- [19231](https://github.com/apache/superset/pull/19231): The `ENABLE_REACT_CRUD_VIEWS` feature flag has been removed (permanently enabled). Any deployments which had set this flag to false will need to verify that the React views support their use case.
|
||||
- [19230](https://github.com/apache/superset/pull/19230): The `ROW_LEVEL_SECURITY` feature flag has been removed (permanently enabled). Any deployments which had set this flag to false will need to verify that the presence of the Row Level Security feature does not interfere with their use case.
|
||||
- [19168](https://github.com/apache/superset/pull/19168): Celery upgrade to 5.X resulted in breaking changes to its command line invocation. Please follow [these](https://docs.celeryq.dev/en/stable/whatsnew-5.2.html#step-1-adjust-your-command-line-invocation) instructions for adjustments. Also consider migrating you Celery config per [here](https://docs.celeryq.dev/en/stable/userguide/configuration.html#conf-old-settings-map).
|
||||
- [19168](https://github.com/apache/superset/pull/19168): Celery upgrade to 5.X resulted in breaking changes to its command line invocation.
|
||||
html#step-1-adjust-your-command-line-invocation) instructions for adjustments. Also consider migrating you Celery config per [here](https://docs.celeryq.dev/en/stable/userguide/configuration.html#conf-old-settings-map).
|
||||
- [19142](https://github.com/apache/superset/pull/19142): The `VERSIONED_EXPORT` config key is now `True` by default.
|
||||
- [19113](https://github.com/apache/superset/pull/19113): The `ENABLE_JAVASCRIPT_CONTROLS` config key has moved from an app config to a feature flag. Any deployments who overrode this setting will now need to override the feature flag from here onward.
|
||||
- [19107](https://github.com/apache/superset/pull/19107): The `SQLLAB_BACKEND_PERSISTENCE` feature flag is now `True` by default, which enables persisting SQL Lab tabs in the backend instead of the browser's `localStorage`.
|
||||
@@ -539,7 +540,7 @@ assists people when migrating to a new version.
|
||||
- [8117](https://github.com/apache/superset/pull/8117): If you are
|
||||
using `ENABLE_PROXY_FIX = True`, review the newly-introduced variable,
|
||||
`PROXY_FIX_CONFIG`, which changes the proxy behavior in accordance with
|
||||
[Werkzeug](https://werkzeug.palletsprojects.com/en/0.15.x/middleware/proxy_fix/)
|
||||
Werkzeug.
|
||||
|
||||
- [8069](https://github.com/apache/superset/pull/8069): introduces
|
||||
[MessagePack](https://github.com/msgpack/msgpack-python) and
|
||||
|
||||
@@ -26,7 +26,7 @@ fi
|
||||
if [ "$BUILD_SUPERSET_FRONTEND_IN_DOCKER" = "true" ]; then
|
||||
cd /app/superset-frontend
|
||||
npm install -f --no-optional --global webpack webpack-cli
|
||||
npm install -f --no-optional
|
||||
npm install -f
|
||||
|
||||
echo "Running frontend"
|
||||
npm run dev
|
||||
|
||||
@@ -54,7 +54,7 @@ are compatible with Superset.
|
||||
| [Azure MS SQL](/docs/configuration/databases#sql-server) | `pip install pymssql` | `mssql+pymssql://UserName@presetSQL:TestPassword@presetSQL.database.windows.net:1433/TestSchema` |
|
||||
| [ClickHouse](/docs/configuration/databases#clickhouse) | `pip install clickhouse-connect` | `clickhousedb://{username}:{password}@{hostname}:{port}/{database}` |
|
||||
| [CockroachDB](/docs/configuration/databases#cockroachdb) | `pip install cockroachdb` | `cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable` |
|
||||
| [CouchbaseDB](/docs/configuration/databases#couchbaseDB) | `pip install couchbase-sqlalchemy` | `couchbasedb://{username}:{password}@{hostname}:{port}?truststorepath={ssl certificate path}` |
|
||||
| [Couchbase](/docs/configuration/databases#couchbase) | `pip install couchbase-sqlalchemy` | `couchbase://{username}:{password}@{hostname}:{port}?truststorepath={ssl certificate path}` |
|
||||
| [Dremio](/docs/configuration/databases#dremio) | `pip install sqlalchemy_dremio` | `dremio://user:pwd@host:31010/` |
|
||||
| [Elasticsearch](/docs/configuration/databases#elasticsearch) | `pip install elasticsearch-dbapi` | `elasticsearch+http://{user}:{password}@{host}:9200/` |
|
||||
| [Exasol](/docs/configuration/databases#exasol) | `pip install sqlalchemy-exasol` | `exa+pyodbc://{username}:{password}@{hostname}:{port}/my_schema?CONNECTIONLCALL=en_US.UTF-8&driver=EXAODBC` |
|
||||
@@ -375,9 +375,10 @@ cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable
|
||||
|
||||
|
||||
|
||||
#### CouchbaseDB
|
||||
#### Couchbase
|
||||
|
||||
The recommended connector library for CouchbaseDB is
|
||||
The Couchbase's Superset connection is designed to support two services: Couchbase Analytics and Couchbase Columnar.
|
||||
The recommended connector library for couchbase is
|
||||
[couchbase-sqlalchemy](https://github.com/couchbase/couchbase-sqlalchemy).
|
||||
```
|
||||
pip install couchbase-sqlalchemy
|
||||
@@ -386,7 +387,7 @@ pip install couchbase-sqlalchemy
|
||||
The expected connection string is formatted as follows:
|
||||
|
||||
```
|
||||
couchbasedb://{username}:{password}@{hostname}:{port}?truststorepath={certificate path}?ssl={true/false}
|
||||
couchbase://{username}:{password}@{hostname}:{port}?truststorepath={certificate path}?ssl={true/false}
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ text strings from Superset's UI. You can jump into the existing
|
||||
language dictionaries at
|
||||
`superset/translations/<language_code>/LC_MESSAGES/messages.po`, or
|
||||
even create a dictionary for a new language altogether.
|
||||
See [Translating](howtos#contribute-translations) for more details.
|
||||
See [Translating](howtos#contributing-translations) for more details.
|
||||
|
||||
### Ask Questions
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ A philosophy we would like to strongly encourage is
|
||||
|
||||
The purpose is to separate problem from possible solutions.
|
||||
|
||||
**Bug fixes:** If you’re only fixing a small bug, it’s fine to submit a pull request right away but we highly recommend to file an issue detailing what you’re fixing. This is helpful in case we don’t accept that specific fix but want to keep track of the issue. Please keep in mind that the project maintainers reserve the rights to accept or reject incoming PRs, so it is better to separate the issue and the code to fix it from each other. In some cases, project maintainers may request you to create a separate issue from PR before proceeding.
|
||||
**Bug fixes:** If you’re only fixing a small bug, it’s fine to submit a pull request right away but we highly recommend filing an issue detailing what you’re fixing. This is helpful in case we don’t accept that specific fix but want to keep track of the issue. Please keep in mind that the project maintainers reserve the rights to accept or reject incoming PRs, so it is better to separate the issue and the code to fix it from each other. In some cases, project maintainers may request you to create a separate issue from PR before proceeding.
|
||||
|
||||
**Refactor:** For small refactors, it can be a standalone PR itself detailing what you are refactoring and why. If there are concerns, project maintainers may request you to create a `#SIP` for the PR before proceeding.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd ../.. && pwd )"
|
||||
LICENSE_TMP=$(mktemp)
|
||||
@@ -47,6 +48,10 @@ pybabel extract \
|
||||
--copyright-holder=Superset \
|
||||
--project=Superset \
|
||||
-k _ -k __ -k t -k tn:1,2 -k tct .
|
||||
|
||||
# Normalize .pot file
|
||||
msgcat --sort-by-msgid --no-wrap --no-location superset/translations/messages.pot -o superset/translations/messages.pot
|
||||
|
||||
cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \
|
||||
&& mv messages.pot.tmp superset/translations/messages.pot
|
||||
|
||||
|
||||
@@ -16,10 +16,19 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { SAMPLE_DASHBOARD_1, TABBED_DASHBOARD } from 'cypress/utils/urls';
|
||||
import {
|
||||
SAMPLE_DASHBOARD_1,
|
||||
SUPPORTED_CHARTS_DASHBOARD,
|
||||
TABBED_DASHBOARD,
|
||||
} from 'cypress/utils/urls';
|
||||
import { drag, resize, waitForChartLoad } from 'cypress/utils';
|
||||
import * as ace from 'brace';
|
||||
import { interceptGet, interceptUpdate, openTab } from './utils';
|
||||
import {
|
||||
interceptExploreUpdate,
|
||||
interceptGet,
|
||||
interceptUpdate,
|
||||
openTab,
|
||||
} from './utils';
|
||||
import {
|
||||
interceptExploreJson,
|
||||
interceptFiltering as interceptCharts,
|
||||
@@ -42,15 +51,37 @@ function openProperties() {
|
||||
cy.getBySel('header-actions-menu')
|
||||
.contains('Edit properties')
|
||||
.click({ force: true });
|
||||
cy.wait(500);
|
||||
cy.get('.ant-modal-body').should('be.visible');
|
||||
});
|
||||
}
|
||||
|
||||
function openExploreProperties() {
|
||||
cy.getBySel('actions-trigger').click({ force: true });
|
||||
cy.get('.ant-dropdown-menu')
|
||||
.contains('Edit chart properties')
|
||||
.click({ force: true });
|
||||
cy.get('.ant-modal-body').should('be.visible');
|
||||
}
|
||||
|
||||
function assertMetadata(text: string) {
|
||||
const regex = new RegExp(text);
|
||||
cy.get('#json_metadata')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
const metadata = cy.$$('#json_metadata')[0];
|
||||
|
||||
// cypress can read this locally, but not in ci
|
||||
// so we have to use the ace module directly to fetch the value
|
||||
expect(ace.edit(metadata).getValue()).to.match(regex);
|
||||
});
|
||||
}
|
||||
|
||||
function openAdvancedProperties() {
|
||||
cy.get('.ant-modal-body')
|
||||
.contains('Advanced')
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
cy.get('#json_metadata').should('be.visible');
|
||||
}
|
||||
|
||||
function dragComponent(
|
||||
@@ -83,20 +114,36 @@ function visitEdit(sampleDashboard = SAMPLE_DASHBOARD_1) {
|
||||
cy.visit(sampleDashboard);
|
||||
cy.wait('@get');
|
||||
editDashboard();
|
||||
cy.get('.grid-container').should('exist');
|
||||
cy.wait('@filtering');
|
||||
cy.wait(500);
|
||||
}
|
||||
|
||||
function resetTabbedDashboard(go = false) {
|
||||
function visit(sampleDashboard = SAMPLE_DASHBOARD_1) {
|
||||
interceptCharts();
|
||||
interceptGet();
|
||||
|
||||
if (sampleDashboard === SAMPLE_DASHBOARD_1) {
|
||||
cy.createSampleDashboards([0]);
|
||||
}
|
||||
|
||||
cy.visit(sampleDashboard);
|
||||
cy.wait('@get');
|
||||
cy.get('.grid-container').should('exist');
|
||||
cy.wait(500);
|
||||
}
|
||||
|
||||
function resetDashboardColors(dashboard = 'tabbed_dash') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
cy.getDashboard('tabbed_dash').then((r: Record<string, any>) => {
|
||||
cy.getDashboard(dashboard).then((r: Record<string, any>) => {
|
||||
const jsonMetadata = r?.json_metadata || '{}';
|
||||
const metadata = JSON.parse(jsonMetadata);
|
||||
const resetMetadata = JSON.stringify({
|
||||
...metadata,
|
||||
color_scheme: '',
|
||||
label_colors: {},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
map_label_colors: {},
|
||||
});
|
||||
cy.updateDashboard(r.id, {
|
||||
certification_details: r.certification_details,
|
||||
@@ -106,25 +153,35 @@ function resetTabbedDashboard(go = false) {
|
||||
json_metadata: resetMetadata,
|
||||
owners: r.owners,
|
||||
slug: r.slug,
|
||||
}).then(() => {
|
||||
if (go) {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function visitResetTabbedDashboard() {
|
||||
resetTabbedDashboard(true);
|
||||
function selectColorScheme(
|
||||
color: string,
|
||||
target = 'dashboard-edit-properties-form',
|
||||
) {
|
||||
cy.get(`[data-test="${target}"] input[aria-label="Select color scheme"]`)
|
||||
.first()
|
||||
.then($input => {
|
||||
cy.wrap($input).click({ force: true });
|
||||
cy.wrap($input).type(color.slice(0, 5), { force: true });
|
||||
});
|
||||
cy.getBySel(color).click({ force: true });
|
||||
}
|
||||
|
||||
function selectColorScheme(color: string) {
|
||||
cy.get(
|
||||
'[data-test="dashboard-edit-properties-form"] [aria-label="Select color scheme"]',
|
||||
)
|
||||
.first()
|
||||
.click();
|
||||
cy.getBySel(color).click({ force: true });
|
||||
function saveAndGo(dashboard = 'Tabbed Dashboard') {
|
||||
interceptExploreUpdate();
|
||||
cy.getBySel('query-save-button').click();
|
||||
cy.getBySel('save-modal-body').then($modal => {
|
||||
cy.wrap($modal)
|
||||
.find("div[aria-label='Select a dashboard'] .ant-select-selection-item")
|
||||
.should('have.text', dashboard);
|
||||
cy.getBySel('save-overwrite-radio').should('not.be.disabled');
|
||||
cy.getBySel('save-overwrite-radio').click();
|
||||
cy.get('#btn_modal_save_goto_dash').click();
|
||||
cy.wait('@chartUpdate');
|
||||
});
|
||||
}
|
||||
|
||||
function applyChanges() {
|
||||
@@ -137,37 +194,37 @@ function saveChanges() {
|
||||
cy.wait('@update');
|
||||
}
|
||||
|
||||
function assertMetadata(text: string) {
|
||||
const regex = new RegExp(text);
|
||||
cy.get('#json_metadata')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
const metadata = cy.$$('#json_metadata')[0];
|
||||
|
||||
// cypress can read this locally, but not in ci
|
||||
// so we have to use the ace module directly to fetch the value
|
||||
expect(ace.edit(metadata).getValue()).to.match(regex);
|
||||
});
|
||||
}
|
||||
function clearMetadata() {
|
||||
cy.get('#json_metadata').then($jsonmetadata => {
|
||||
cy.wrap($jsonmetadata).find('.ace_content').click();
|
||||
cy.wrap($jsonmetadata).find('.ace_content').click({ force: true });
|
||||
cy.wrap($jsonmetadata)
|
||||
.find('.ace_text-input')
|
||||
.type('{selectall} {backspace}', { force: true });
|
||||
.then($ace => {
|
||||
cy.wrap($ace).focus();
|
||||
cy.wrap($ace).should('have.focus');
|
||||
cy.wrap($ace).type('{selectall}', { force: true });
|
||||
cy.wrap($ace).type('{backspace}', { force: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeMetadata(metadata: string) {
|
||||
cy.get('#json_metadata').then($jsonmetadata =>
|
||||
cy
|
||||
.wrap($jsonmetadata)
|
||||
cy.get('#json_metadata').then($jsonmetadata => {
|
||||
cy.wrap($jsonmetadata).find('.ace_content').click({ force: true });
|
||||
cy.wrap($jsonmetadata)
|
||||
.find('.ace_text-input')
|
||||
.type(metadata, { parseSpecialCharSequences: false, force: true }),
|
||||
);
|
||||
.then($ace => {
|
||||
cy.wrap($ace).focus();
|
||||
cy.wrap($ace).should('have.focus');
|
||||
cy.wrap($ace).type(metadata, {
|
||||
parseSpecialCharSequences: false,
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openExplore(chartName: string) {
|
||||
function openExploreWithDashboardContext(chartName: string) {
|
||||
interceptExploreJson();
|
||||
interceptGet();
|
||||
|
||||
@@ -181,23 +238,351 @@ function openExplore(chartName: string) {
|
||||
.should('contain', 'Edit chart')
|
||||
.click();
|
||||
cy.wait('@getJson');
|
||||
cy.get('.chart-container').should('exist');
|
||||
}
|
||||
|
||||
function saveExploreColorScheme(
|
||||
chart = 'Top 10 California Names Timeseries',
|
||||
colorScheme = 'supersetColors',
|
||||
) {
|
||||
interceptExploreUpdate();
|
||||
openExploreWithDashboardContext(chart);
|
||||
openTab(0, 1, 'control-tabs');
|
||||
selectColorScheme(colorScheme, 'control-item');
|
||||
cy.getBySel('query-save-button').click();
|
||||
cy.getBySel('save-overwrite-radio').click();
|
||||
cy.getBySel('btn-modal-save').click();
|
||||
cy.wait('@chartUpdate');
|
||||
}
|
||||
|
||||
describe('Dashboard edit', () => {
|
||||
describe('Color consistency', () => {
|
||||
beforeEach(() => {
|
||||
visitResetTabbedDashboard();
|
||||
resetDashboardColors();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
resetTabbedDashboard();
|
||||
it('should not allow to change color scheme of a chart when dashboard has one', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
openTab(0, 1, 'control-tabs');
|
||||
|
||||
cy.get('[aria-label="Select color scheme"]').should('be.disabled');
|
||||
});
|
||||
|
||||
it('should not allow to change color scheme of a chart when dashboard has no scheme but chart has shared labels', () => {
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// open second top tab to catch shared labels
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Trends',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
openTab(0, 0);
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
openTab(0, 1, 'control-tabs');
|
||||
|
||||
cy.get('[aria-label="Select color scheme"]').should('be.disabled');
|
||||
});
|
||||
|
||||
it('should allow to change color scheme of a chart when dashboard has no scheme but only custom label colors', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
writeMetadata('{"color_scheme":"","label_colors":{"Anthony":"red"}}');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
openTab(0, 1, 'control-tabs');
|
||||
selectColorScheme('blueToGreen', 'control-item');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
// label Christopher
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// label Daniel
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(0, 76, 218)');
|
||||
|
||||
// label David
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.eq(3)
|
||||
.should('have.css', 'fill', 'rgb(0, 116, 241)');
|
||||
});
|
||||
|
||||
it('should allow to change color scheme of a chart when dashboard has no scheme and show the change', () => {
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
openTab(0, 1, 'control-tabs');
|
||||
selectColorScheme('blueToGreen', 'control-item');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
saveAndGo();
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// reset original scheme
|
||||
saveExploreColorScheme();
|
||||
});
|
||||
|
||||
it('should allow to change color scheme of a chart when dashboard has no scheme but custom label colors and show the change', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
writeMetadata('{"color_scheme":"","label_colors":{"Anthony":"red"}}');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
openTab(0, 1, 'control-tabs');
|
||||
selectColorScheme('blueToGreen', 'control-item');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
// label Christopher
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
saveAndGo();
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
// label Christopher
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// reset original scheme
|
||||
saveExploreColorScheme();
|
||||
});
|
||||
|
||||
it('should not change colors on refreshes with no color scheme set', () => {
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Andrew
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(69, 78, 124)');
|
||||
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Andrew
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(69, 78, 124)');
|
||||
});
|
||||
|
||||
it('should not change colors on refreshes with color scheme set', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Andrew
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(0, 76, 218)');
|
||||
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Andrew
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(1)
|
||||
.should('have.css', 'fill', 'rgb(0, 76, 218)');
|
||||
});
|
||||
|
||||
it('should respect chart color scheme when none is set for the dashboard', () => {
|
||||
openProperties();
|
||||
cy.get('[aria-label="Select color scheme"]').should('have.value', '');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
@@ -214,7 +599,8 @@ describe('Dashboard edit', () => {
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
});
|
||||
|
||||
it('should apply same color to same labels with color scheme set', () => {
|
||||
it('should apply same color to same labels with color scheme set on refresh', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
@@ -238,17 +624,63 @@ describe('Dashboard edit', () => {
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
visit(TABBED_DASHBOARD);
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
});
|
||||
|
||||
it('should apply same color to same labels with no color scheme set', () => {
|
||||
openProperties();
|
||||
cy.get('[aria-label="Select color scheme"]').should('have.value', '');
|
||||
applyChanges();
|
||||
saveChanges();
|
||||
it('should apply same color to same labels with no color scheme set on refresh', () => {
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
name: 'Top 10 California Names Timeseries',
|
||||
viz: 'line',
|
||||
});
|
||||
|
||||
// label Anthony
|
||||
cy.get(
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
waitForChartLoad({ name: 'Trends', viz: 'line' });
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(31, 168, 201)');
|
||||
|
||||
visit(TABBED_DASHBOARD);
|
||||
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
@@ -275,6 +707,7 @@ describe('Dashboard edit', () => {
|
||||
});
|
||||
|
||||
it('custom label colors should take the precedence in nested tabs', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
@@ -305,6 +738,7 @@ describe('Dashboard edit', () => {
|
||||
});
|
||||
|
||||
it('label colors should take the precedence for rendered charts in nested tabs', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
// open the tab first time and let chart load
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
@@ -333,6 +767,7 @@ describe('Dashboard edit', () => {
|
||||
});
|
||||
|
||||
it('should re-apply original color after removing custom label color with color scheme set', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
@@ -375,6 +810,7 @@ describe('Dashboard edit', () => {
|
||||
});
|
||||
|
||||
it('should re-apply original color after removing custom label color with no color scheme set', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
// open nested tab
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
@@ -438,6 +874,7 @@ describe('Dashboard edit', () => {
|
||||
});
|
||||
|
||||
it('should show the same colors in Explore', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
@@ -461,7 +898,7 @@ describe('Dashboard edit', () => {
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
|
||||
openExplore('Top 10 California Names Timeseries');
|
||||
openExploreWithDashboardContext('Top 10 California Names Timeseries');
|
||||
|
||||
// label Anthony
|
||||
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
|
||||
@@ -469,7 +906,8 @@ describe('Dashboard edit', () => {
|
||||
.should('have.css', 'fill', 'rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
it.skip('should change color scheme multiple times', () => {
|
||||
it('should change color scheme multiple times', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
@@ -487,7 +925,7 @@ describe('Dashboard edit', () => {
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// open 2nd main tab
|
||||
openTab(0, 1);
|
||||
@@ -496,7 +934,7 @@ describe('Dashboard edit', () => {
|
||||
// label Anthony
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
editDashboard();
|
||||
openProperties();
|
||||
@@ -507,7 +945,7 @@ describe('Dashboard edit', () => {
|
||||
// label Anthony
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.eq(2)
|
||||
.should('have.css', 'fill', 'rgb(41, 105, 107)');
|
||||
.should('have.css', 'fill', 'rgb(0, 128, 246)');
|
||||
|
||||
// open main tab and nested tab
|
||||
openTab(0, 0);
|
||||
@@ -518,10 +956,11 @@ describe('Dashboard edit', () => {
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(41, 105, 107)');
|
||||
.should('have.css', 'fill', 'rgb(0, 128, 246)');
|
||||
});
|
||||
|
||||
it.skip('should apply the color scheme across main tabs', () => {
|
||||
it('should apply the color scheme across main tabs', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
@@ -533,10 +972,11 @@ describe('Dashboard edit', () => {
|
||||
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
});
|
||||
|
||||
it.skip('should apply the color scheme across main tabs for rendered charts', () => {
|
||||
it('should apply the color scheme across main tabs for rendered charts', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
waitForChartLoad({ name: 'Treemap', viz: 'treemap_v2' });
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
@@ -549,7 +989,7 @@ describe('Dashboard edit', () => {
|
||||
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(41, 105, 107)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// change scheme now that charts are rendered across the main tabs
|
||||
editDashboard();
|
||||
@@ -560,10 +1000,11 @@ describe('Dashboard edit', () => {
|
||||
|
||||
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(0, 128, 246)');
|
||||
});
|
||||
|
||||
it.skip('should apply the color scheme in nested tabs', () => {
|
||||
it('should apply the color scheme in nested tabs', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
openProperties();
|
||||
selectColorScheme('blueToGreen');
|
||||
applyChanges();
|
||||
@@ -579,17 +1020,18 @@ describe('Dashboard edit', () => {
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
|
||||
// open another nested tab
|
||||
openTab(2, 1);
|
||||
waitForChartLoad({ name: 'Growth Rate', viz: 'line' });
|
||||
cy.get('[data-test-chart-name="Growth Rate"] .line .nv-legend-symbol')
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
});
|
||||
|
||||
it.skip('should apply a valid color scheme for rendered charts in nested tabs', () => {
|
||||
it('should apply a valid color scheme for rendered charts in nested tabs', () => {
|
||||
visitEdit(TABBED_DASHBOARD);
|
||||
// open the tab first time and let chart load
|
||||
openTab(1, 1);
|
||||
waitForChartLoad({
|
||||
@@ -611,7 +1053,7 @@ describe('Dashboard edit', () => {
|
||||
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
|
||||
)
|
||||
.first()
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
.should('have.css', 'fill', 'rgb(50, 0, 167)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -623,9 +1065,10 @@ describe('Dashboard edit', () => {
|
||||
beforeEach(() => {
|
||||
cy.createSampleDashboards([0]);
|
||||
openProperties();
|
||||
selectColorScheme('supersetColors');
|
||||
});
|
||||
|
||||
it.skip('should accept a valid color scheme', () => {
|
||||
it('should accept a valid color scheme', () => {
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
writeMetadata('{"color_scheme":"lyftColors"}');
|
||||
@@ -636,21 +1079,21 @@ describe('Dashboard edit', () => {
|
||||
applyChanges();
|
||||
});
|
||||
|
||||
it.skip('should overwrite the color scheme when advanced is closed', () => {
|
||||
it('should overwrite the color scheme when advanced is closed', () => {
|
||||
selectColorScheme('blueToGreen');
|
||||
openAdvancedProperties();
|
||||
assertMetadata('blueToGreen');
|
||||
applyChanges();
|
||||
});
|
||||
|
||||
it.skip('should overwrite the color scheme when advanced is open', () => {
|
||||
it('should overwrite the color scheme when advanced is open', () => {
|
||||
openAdvancedProperties();
|
||||
selectColorScheme('modernSunset');
|
||||
assertMetadata('modernSunset');
|
||||
applyChanges();
|
||||
});
|
||||
|
||||
it.skip('should not accept an invalid color scheme', () => {
|
||||
it('should not accept an invalid color scheme', () => {
|
||||
openAdvancedProperties();
|
||||
clearMetadata();
|
||||
// allow console error
|
||||
@@ -714,7 +1157,7 @@ describe('Dashboard edit', () => {
|
||||
visitEdit();
|
||||
});
|
||||
|
||||
it.skip('should add charts', () => {
|
||||
it('should add charts', () => {
|
||||
cy.get('[role="checkbox"]').click();
|
||||
dragComponent();
|
||||
cy.getBySel('dashboard-component-chart-holder').should('have.length', 1);
|
||||
@@ -762,7 +1205,7 @@ describe('Dashboard edit', () => {
|
||||
visitEdit();
|
||||
});
|
||||
|
||||
it.skip('should save', () => {
|
||||
it('should save', () => {
|
||||
cy.get('[role="checkbox"]').click();
|
||||
dragComponent();
|
||||
cy.getBySel('header-save-button').should('be.enabled');
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
23
superset-frontend/package-lock.json
generated
23
superset-frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "superset",
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -55,8 +55,7 @@ const TooltipSection = ({
|
||||
text: ReactNode;
|
||||
}) => (
|
||||
<TooltipSectionWrapper>
|
||||
<TooltipSectionLabel>{label}</TooltipSectionLabel>
|
||||
<span>{text}</span>
|
||||
<TooltipSectionLabel>{label}</TooltipSectionLabel>: <span>{text}</span>
|
||||
</TooltipSectionWrapper>
|
||||
);
|
||||
|
||||
@@ -71,12 +70,7 @@ export const getColumnTypeTooltipNode = (column: ColumnMeta): ReactNode => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipSection
|
||||
label={t('Column datatype')}
|
||||
text={column.type.toLowerCase()}
|
||||
/>
|
||||
);
|
||||
return <TooltipSection label={t('Column type')} text={column.type} />;
|
||||
};
|
||||
|
||||
export const getColumnTooltipNode = (
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
import {
|
||||
formatSelectOptions,
|
||||
displayTimeRelatedControls,
|
||||
getColorControlsProps,
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
@@ -142,9 +143,7 @@ const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
|
||||
renderTrigger: true,
|
||||
schemes: () => sequentialSchemeRegistry.getMap(),
|
||||
isLinear: true,
|
||||
mapStateToProps: state => ({
|
||||
dashboardId: state?.form_data?.dashboardId,
|
||||
}),
|
||||
mapStateToProps: state => getColorControlsProps(state),
|
||||
};
|
||||
|
||||
const granularity: SharedControlConfig<'SelectControl'> = {
|
||||
@@ -333,9 +332,7 @@ const color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
|
||||
choices: () => categoricalSchemeRegistry.keys().map(s => [s, s]),
|
||||
description: t('The color scheme for rendering chart'),
|
||||
schemes: () => categoricalSchemeRegistry.getMap(),
|
||||
mapStateToProps: state => ({
|
||||
dashboardId: state?.form_data?.dashboardId,
|
||||
}),
|
||||
mapStateToProps: state => getColorControlsProps(state),
|
||||
};
|
||||
|
||||
const truncate_metric: SharedControlConfig<'CheckboxControl'> = {
|
||||
|
||||
@@ -87,6 +87,9 @@ export interface Dataset {
|
||||
}
|
||||
|
||||
export interface ControlPanelState {
|
||||
slice: {
|
||||
slice_id: number;
|
||||
};
|
||||
form_data: QueryFormData;
|
||||
datasource: Dataset | QueryResponse | null;
|
||||
controls: ControlStateMapping;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export const getColorControlsProps = (state: Record<string, any>) => {
|
||||
const dashboardId = state?.form_data?.dashboardId;
|
||||
return {
|
||||
chartId: state?.slice?.slice_id,
|
||||
dashboardId,
|
||||
hasDashboardColorScheme:
|
||||
!!dashboardId && !!state?.form_data?.dashboard_color_scheme,
|
||||
hasCustomLabelsColor:
|
||||
Object.keys(state?.form_data?.label_colors || {}).length > 0,
|
||||
colorNamespace: state?.form_data?.color_namespace,
|
||||
mapLabelsColors: state?.form_data?.map_label_colors || {},
|
||||
sharedLabelsColors: state?.form_data?.shared_label_colors || [],
|
||||
};
|
||||
};
|
||||
@@ -27,3 +27,4 @@ export * from './defineSavedMetrics';
|
||||
export * from './getStandardizedControls';
|
||||
export * from './getTemporalColumns';
|
||||
export { default as displayTimeRelatedControls } from './displayTimeRelatedControls';
|
||||
export * from './colorControls';
|
||||
|
||||
@@ -90,7 +90,7 @@ test('should get column datatype rendered as tooltip when column has a type', ()
|
||||
</>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Column datatype')).toBeVisible();
|
||||
expect(screen.getByText('Column type')).toBeVisible();
|
||||
expect(screen.getByText('text')).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { getColorControlsProps } from '../../src';
|
||||
|
||||
describe('getColorControlsProps', () => {
|
||||
it('should return default values when state is empty', () => {
|
||||
const state = {};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: undefined,
|
||||
dashboardId: undefined,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: false,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct values when state has form_data with dashboardId and color scheme', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 123,
|
||||
dashboard_color_scheme: 'blueScheme',
|
||||
label_colors: {},
|
||||
},
|
||||
slice: { slice_id: 456 },
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: 456,
|
||||
dashboardId: 123,
|
||||
hasDashboardColorScheme: true,
|
||||
hasCustomLabelsColor: false,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect custom label colors correctly', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 123,
|
||||
label_colors: { label1: '#000000' },
|
||||
},
|
||||
slice: { slice_id: 456 },
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: 456,
|
||||
dashboardId: 123,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: true,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return shared label colors when available', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
shared_label_colors: ['#FF5733', '#33FF57'],
|
||||
},
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: undefined,
|
||||
dashboardId: undefined,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: false,
|
||||
sharedLabelsColors: ['#FF5733', '#33FF57'],
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing form_data and slice properties', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 789,
|
||||
},
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: undefined,
|
||||
dashboardId: 789,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: false,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import CategoricalColorScale from './CategoricalColorScale';
|
||||
import { ColorsLookup } from './types';
|
||||
import getCategoricalSchemeRegistry from './CategoricalSchemeRegistrySingleton';
|
||||
@@ -37,10 +38,21 @@ export default class CategoricalColorNamespace {
|
||||
this.forcedItems = {};
|
||||
}
|
||||
|
||||
getScale(schemeId?: string) {
|
||||
const id = schemeId ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
|
||||
/**
|
||||
* A new CategoricalColorScale instance is created for each chart.
|
||||
*
|
||||
* @param colorScheme - the color scheme to use
|
||||
* @returns a new instance of a color scale
|
||||
*/
|
||||
getScale(colorScheme?: string) {
|
||||
const id =
|
||||
colorScheme ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
|
||||
const scheme = getCategoricalSchemeRegistry().get(id);
|
||||
return new CategoricalColorScale(scheme?.colors ?? [], this.forcedItems);
|
||||
return new CategoricalColorScale(
|
||||
scheme?.colors ?? [],
|
||||
this.forcedItems,
|
||||
colorScheme,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,6 +71,17 @@ export default class CategoricalColorNamespace {
|
||||
resetColors() {
|
||||
this.forcedItems = {};
|
||||
}
|
||||
|
||||
resetColorsForLabels(labels: string[] = []) {
|
||||
const updatedForcedItems = cloneDeep(this.forcedItems);
|
||||
labels.forEach(label => {
|
||||
if (updatedForcedItems.hasOwnProperty(label)) {
|
||||
delete updatedForcedItems[label];
|
||||
}
|
||||
});
|
||||
|
||||
this.forcedItems = { ...updatedForcedItems };
|
||||
}
|
||||
}
|
||||
|
||||
const namespaces: {
|
||||
@@ -80,16 +103,19 @@ export function getNamespace(name: string = DEFAULT_NAMESPACE) {
|
||||
|
||||
export function getColor(
|
||||
value?: string,
|
||||
schemeId?: string,
|
||||
colorScheme?: string,
|
||||
namespace?: string,
|
||||
) {
|
||||
return getNamespace(namespace).getScale(schemeId).getColor(value);
|
||||
return getNamespace(namespace).getScale(colorScheme).getColor(value);
|
||||
}
|
||||
|
||||
/*
|
||||
Returns a new scale instance within the same namespace.
|
||||
Especially useful when a chart is booting for the first time
|
||||
|
||||
@param scheme - the applied color scheme
|
||||
@param namespace - the namespace
|
||||
*/
|
||||
export function getScale(scheme?: string, namespace?: string) {
|
||||
return getNamespace(namespace).getScale(scheme);
|
||||
export function getScale(colorScheme?: string, namespace?: string) {
|
||||
return getNamespace(namespace).getScale(colorScheme);
|
||||
}
|
||||
|
||||
@@ -21,14 +21,16 @@ import { scaleOrdinal, ScaleOrdinal } from 'd3-scale';
|
||||
import { ExtensibleFunction } from '../models';
|
||||
import { ColorsInitLookup, ColorsLookup } from './types';
|
||||
import stringifyAndTrim from './stringifyAndTrim';
|
||||
import getLabelsColorMap from './LabelsColorMapSingleton';
|
||||
import getLabelsColorMap, {
|
||||
LabelsColorMapSource,
|
||||
} from './LabelsColorMapSingleton';
|
||||
import { getAnalogousColors } from './utils';
|
||||
import { FeatureFlag, isFeatureEnabled } from '../utils';
|
||||
|
||||
// Use type augmentation to correct the fact that
|
||||
// an instance of CategoricalScale is also a function
|
||||
interface CategoricalColorScale {
|
||||
(x: { toString(): string }, y?: number, w?: string): string;
|
||||
(x: { toString(): string }, y?: number): string;
|
||||
}
|
||||
|
||||
class CategoricalColorScale extends ExtensibleFunction {
|
||||
@@ -50,11 +52,16 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
* Constructor
|
||||
* @param {*} colors an array of colors
|
||||
* @param {*} forcedColors optional parameter that comes from parent
|
||||
* (usually CategoricalColorNamespace)
|
||||
* @param {*} appliedColorScheme the color scheme applied to the chart
|
||||
*
|
||||
*/
|
||||
constructor(colors: string[], forcedColors: ColorsInitLookup = {}) {
|
||||
super((value: string, sliceId?: number, colorScheme?: string) =>
|
||||
this.getColor(value, sliceId, colorScheme),
|
||||
constructor(
|
||||
colors: string[],
|
||||
forcedColors: ColorsInitLookup = {},
|
||||
appliedColorScheme?: string,
|
||||
) {
|
||||
super((value: string, sliceId?: number) =>
|
||||
this.getColor(value, sliceId, appliedColorScheme),
|
||||
);
|
||||
// holds original color scheme colors
|
||||
this.originColors = colors;
|
||||
@@ -107,15 +114,28 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
*
|
||||
* @param value the value of a label to get the color for
|
||||
* @param sliceId the ID of the current chart
|
||||
* @param colorScheme the original color scheme of the chart
|
||||
* @param appliedColorScheme the color scheme applied to the chart
|
||||
* @returns the color or the next available color
|
||||
*/
|
||||
getColor(value?: string, sliceId?: number, colorScheme?: string): string {
|
||||
getColor(
|
||||
value?: string,
|
||||
sliceId?: number,
|
||||
appliedColorScheme?: string,
|
||||
): string {
|
||||
const cleanedValue = stringifyAndTrim(value);
|
||||
// priority: forced color (i.e. custom label colors) > shared color > scale color
|
||||
// priority: forced color (aka custom label colors) > shared color > scale color
|
||||
const forcedColor = this.forcedColors?.[cleanedValue];
|
||||
const isExistingLabel = this.chartLabelsColorMap.has(cleanedValue);
|
||||
let color = forcedColor || this.scale(cleanedValue);
|
||||
const { source } = this.labelsColorMapInstance;
|
||||
const currentColorMap =
|
||||
source === LabelsColorMapSource.Dashboard
|
||||
? this.labelsColorMapInstance.getColorMap()
|
||||
: this.chartLabelsColorMap;
|
||||
const isExistingLabel = currentColorMap.has(cleanedValue);
|
||||
let color =
|
||||
forcedColor ||
|
||||
(isExistingLabel
|
||||
? (currentColorMap.get(cleanedValue) as string)
|
||||
: this.scale(cleanedValue));
|
||||
|
||||
// a forced color will always be used independently of the usage count
|
||||
if (!forcedColor && !isExistingLabel) {
|
||||
@@ -128,7 +148,7 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
this.isColorUsed(color)
|
||||
) {
|
||||
// fallback to least used color
|
||||
color = this.getNextAvailableColor(color);
|
||||
color = this.getNextAvailableColor(cleanedValue, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +161,7 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
cleanedValue,
|
||||
color,
|
||||
sliceId,
|
||||
colorScheme,
|
||||
appliedColorScheme,
|
||||
);
|
||||
}
|
||||
return color;
|
||||
@@ -164,48 +184,76 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
* @param color the color to check
|
||||
* @returns the count of the color usage in this slice
|
||||
*/
|
||||
getColorUsageCount(currentColor: string): number {
|
||||
let count = 0;
|
||||
this.chartLabelsColorMap.forEach(color => {
|
||||
if (color === currentColor) {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
getColorUsageCount(color: string): number {
|
||||
return Array.from(this.chartLabelsColorMap.values()).filter(
|
||||
value => value === color,
|
||||
).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lower chances of color collision by returning the least used color
|
||||
* Checks across colors of current slice within LabelsColorMapSingleton
|
||||
* Lower chances of color collision by returning the least used color.
|
||||
* Checks across colors of current slice within chartLabelsColorMap.
|
||||
*
|
||||
* @param currentLabel the current label
|
||||
* @param currentColor the current color
|
||||
* @returns the least used color that is not the excluded color
|
||||
* @returns the least used color that is not the current color
|
||||
*/
|
||||
getNextAvailableColor(currentColor: string) {
|
||||
const colorUsageArray = this.colors.map(color => ({
|
||||
color,
|
||||
count: this.getColorUsageCount(color),
|
||||
}));
|
||||
const currentColorCount = this.getColorUsageCount(currentColor);
|
||||
const otherColors = colorUsageArray.filter(
|
||||
colorEntry => colorEntry.color !== currentColor,
|
||||
);
|
||||
// all other colors are used as much or more than currentColor
|
||||
const hasNoneAvailable = otherColors.every(
|
||||
colorEntry => colorEntry.count >= currentColorCount,
|
||||
getNextAvailableColor(currentLabel: string, currentColor: string): string {
|
||||
// Precompute color usage counts for all colors
|
||||
const colorUsageCounts = new Map(
|
||||
this.colors.map(color => [color, this.getColorUsageCount(color)]),
|
||||
);
|
||||
|
||||
// fallback to currentColor color
|
||||
if (!otherColors.length || hasNoneAvailable) {
|
||||
return currentColor;
|
||||
// Get an ordered array of labels from the map
|
||||
const orderedLabels = Array.from(this.chartLabelsColorMap.keys());
|
||||
const currentLabelIndex = orderedLabels.indexOf(currentLabel);
|
||||
|
||||
// Helper to infer "previous" and "next" labels based on index
|
||||
const getAdjacentLabelsColors = (): string[] => {
|
||||
const previousLabel =
|
||||
currentLabelIndex > 0 ? orderedLabels[currentLabelIndex - 1] : null;
|
||||
const nextLabel =
|
||||
currentLabelIndex < orderedLabels.length - 1
|
||||
? orderedLabels[currentLabelIndex + 1]
|
||||
: null;
|
||||
|
||||
const previousColor = previousLabel
|
||||
? this.chartLabelsColorMap.get(previousLabel)
|
||||
: null;
|
||||
const nextColor = nextLabel
|
||||
? this.chartLabelsColorMap.get(nextLabel)
|
||||
: null;
|
||||
|
||||
return [previousColor, nextColor].filter(color => color) as string[];
|
||||
};
|
||||
|
||||
const adjacentColors = getAdjacentLabelsColors();
|
||||
|
||||
// Determine adjusted score (usage count + penalties)
|
||||
const calculateScore = (color: string): number => {
|
||||
/* istanbul ignore next */
|
||||
const usageCount = colorUsageCounts.get(color) || 0;
|
||||
const adjacencyPenalty = adjacentColors.includes(color) ? 100 : 0;
|
||||
return usageCount + adjacencyPenalty;
|
||||
};
|
||||
|
||||
// If there is any color that has never been used, prioritize it
|
||||
const unusedColor = this.colors.find(
|
||||
color => (colorUsageCounts.get(color) || 0) === 0,
|
||||
);
|
||||
if (unusedColor) {
|
||||
return unusedColor;
|
||||
}
|
||||
|
||||
// Finding the least used color
|
||||
const leastUsedColor = otherColors.reduce((min, entry) =>
|
||||
entry.count < min.count ? entry : min,
|
||||
).color;
|
||||
// If all colors are used, calculate scores and choose the best one
|
||||
const otherColors = this.colors.filter(color => color !== currentColor);
|
||||
|
||||
return leastUsedColor;
|
||||
// Find the color with the minimum score, defaulting to currentColor
|
||||
return otherColors.reduce((bestColor, color) => {
|
||||
const bestScore = calculateScore(bestColor);
|
||||
const currentScore = calculateScore(color);
|
||||
return currentScore < bestScore ? color : bestColor;
|
||||
}, currentColor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import { makeSingleton } from '../utils';
|
||||
import CategoricalColorNamespace from './CategoricalColorNamespace';
|
||||
|
||||
export enum LabelsColorMapSource {
|
||||
Dashboard,
|
||||
@@ -25,7 +26,10 @@ export enum LabelsColorMapSource {
|
||||
}
|
||||
|
||||
export class LabelsColorMap {
|
||||
chartsLabelsMap: Map<number, { labels: string[]; scheme?: string }>;
|
||||
chartsLabelsMap: Map<
|
||||
number,
|
||||
{ labels: string[]; scheme?: string; ownScheme?: string }
|
||||
>;
|
||||
|
||||
colorMap: Map<string, string>;
|
||||
|
||||
@@ -38,17 +42,38 @@ export class LabelsColorMap {
|
||||
this.source = LabelsColorMapSource.Dashboard;
|
||||
}
|
||||
|
||||
updateColorMap(categoricalNamespace: any, colorScheme?: string) {
|
||||
const newColorMap = new Map();
|
||||
this.colorMap.clear();
|
||||
/**
|
||||
* Wipes out the color map and updates it with the new color scheme.
|
||||
*
|
||||
* @param categoricalNamespace - the namespace to use for color mapping
|
||||
* @param colorScheme - color scheme
|
||||
*/
|
||||
updateColorMap(
|
||||
categoricalNamespace: CategoricalColorNamespace,
|
||||
colorScheme?: string,
|
||||
merge = false,
|
||||
) {
|
||||
const newColorMap = this.colorMap;
|
||||
|
||||
if (!merge) {
|
||||
newColorMap.clear();
|
||||
}
|
||||
|
||||
this.chartsLabelsMap.forEach((chartConfig, sliceId) => {
|
||||
const { labels, scheme: originalChartColorScheme } = chartConfig;
|
||||
const currentColorScheme = colorScheme || originalChartColorScheme;
|
||||
const colorScale = categoricalNamespace.getScale(currentColorScheme);
|
||||
const { labels, ownScheme } = chartConfig;
|
||||
const appliedColorScheme = colorScheme || ownScheme;
|
||||
const colorScale = categoricalNamespace.getScale(appliedColorScheme);
|
||||
|
||||
labels.forEach(label => {
|
||||
const newColor = colorScale.getColor(label, sliceId);
|
||||
newColorMap.set(label, newColor);
|
||||
// if merge, apply the scheme only to new labels in the map
|
||||
if (!merge || !this.colorMap.has(label)) {
|
||||
const newColor = colorScale.getColor(
|
||||
label,
|
||||
sliceId,
|
||||
appliedColorScheme,
|
||||
);
|
||||
newColorMap.set(label, newColor);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.colorMap = newColorMap;
|
||||
@@ -58,29 +83,63 @@ export class LabelsColorMap {
|
||||
return this.colorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Called individually by each plugin via getColor fn.
|
||||
*
|
||||
* @param label - the label name
|
||||
* @param color - the color
|
||||
* @param sliceId - the chart id
|
||||
* @param colorScheme - the color scheme
|
||||
*
|
||||
*/
|
||||
addSlice(
|
||||
label: string,
|
||||
color: string,
|
||||
sliceId: number,
|
||||
colorScheme?: string,
|
||||
) {
|
||||
if (this.source !== LabelsColorMapSource.Dashboard) return;
|
||||
|
||||
const chartConfig = this.chartsLabelsMap.get(sliceId) || {
|
||||
labels: [],
|
||||
scheme: '',
|
||||
scheme: undefined,
|
||||
ownScheme: undefined,
|
||||
};
|
||||
|
||||
const { labels } = chartConfig;
|
||||
if (!labels.includes(label)) {
|
||||
labels.push(label);
|
||||
this.chartsLabelsMap.set(sliceId, {
|
||||
labels,
|
||||
scheme: colorScheme,
|
||||
ownScheme: chartConfig.ownScheme,
|
||||
});
|
||||
}
|
||||
this.colorMap.set(label, color);
|
||||
if (this.source === LabelsColorMapSource.Dashboard) {
|
||||
this.colorMap.set(label, color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to make sure all slices respect their original scheme.
|
||||
*
|
||||
* @param sliceId - the chart id
|
||||
* @param ownScheme - the color scheme
|
||||
*/
|
||||
setOwnColorScheme(sliceId: number, ownScheme: string) {
|
||||
const chartConfig = this.chartsLabelsMap.get(sliceId);
|
||||
if (chartConfig) {
|
||||
this.chartsLabelsMap.set(sliceId, {
|
||||
...chartConfig,
|
||||
ownScheme,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a slice from the color map.
|
||||
*
|
||||
* @param sliceId - the chart
|
||||
*/
|
||||
removeSlice(sliceId: number) {
|
||||
if (this.source !== LabelsColorMapSource.Dashboard) return;
|
||||
|
||||
@@ -96,10 +155,20 @@ export class LabelsColorMap {
|
||||
this.colorMap = newColorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the shared labels color map.
|
||||
*/
|
||||
clear() {
|
||||
this.chartsLabelsMap.clear();
|
||||
this.colorMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all maps
|
||||
*/
|
||||
reset() {
|
||||
this.clear();
|
||||
this.chartsLabelsMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const getInstance = makeSingleton(LabelsColorMap);
|
||||
|
||||
@@ -161,4 +161,63 @@ describe('CategoricalColorNamespace', () => {
|
||||
expect(color).toBe(color2);
|
||||
});
|
||||
});
|
||||
describe('statis resetColorsForLabels(labels)', () => {
|
||||
it('removes specified labels from forcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.setColor('label2', 'blue');
|
||||
namespace.resetColorsForLabels(['label1']);
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({ label2: 'blue' });
|
||||
});
|
||||
it('does not modify forcedItems if no labels are provided', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.setColor('label2', 'blue');
|
||||
namespace.resetColorsForLabels();
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({
|
||||
label1: 'red',
|
||||
label2: 'blue',
|
||||
});
|
||||
});
|
||||
it('does nothing if the label is not in forcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.resetColorsForLabels(['label2']); // label2 doesn't exist
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({ label1: 'red' });
|
||||
});
|
||||
it('removes all labels when all are provided', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.setColor('label2', 'blue');
|
||||
namespace.resetColorsForLabels(['label1', 'label2']);
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({});
|
||||
});
|
||||
it('creates a deep copy of forcedItems before modifying', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
|
||||
const originalForcedItems = namespace.forcedItems;
|
||||
namespace.resetColorsForLabels(['label1']);
|
||||
|
||||
expect(originalForcedItems).not.toBe(namespace.forcedItems);
|
||||
});
|
||||
it('removes the label if it exists in updatedForcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.resetColorsForLabels(['label1']);
|
||||
|
||||
expect(namespace.forcedItems).toEqual({});
|
||||
});
|
||||
it('does nothing for a label not in updatedForcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.resetColorsForLabels(['label2']); // label2 doesn't exist
|
||||
|
||||
expect(namespace.forcedItems).toEqual({ label1: 'red' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
*/
|
||||
|
||||
import { ScaleOrdinal } from 'd3-scale';
|
||||
import { CategoricalColorScale, FeatureFlag } from '@superset-ui/core';
|
||||
import {
|
||||
CategoricalColorScale,
|
||||
FeatureFlag,
|
||||
LabelsColorMapSource,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
describe('CategoricalColorScale', () => {
|
||||
beforeEach(() => {
|
||||
@@ -43,7 +47,6 @@ describe('CategoricalColorScale', () => {
|
||||
expect(scale).toBeInstanceOf(CategoricalColorScale);
|
||||
expect(scale.forcedColors).toBe(forcedColors);
|
||||
});
|
||||
|
||||
it('can refer to colors based on their index', () => {
|
||||
const forcedColors = { pig: 1, horse: 5 };
|
||||
const scale = new CategoricalColorScale(
|
||||
@@ -67,7 +70,7 @@ describe('CategoricalColorScale', () => {
|
||||
>;
|
||||
let getNextAvailableColorSpy: jest.SpyInstance<
|
||||
string,
|
||||
[currentColor: string]
|
||||
[currentLabel: string, currentColor: string]
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -83,6 +86,36 @@ describe('CategoricalColorScale', () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('uses labelsColorMapInstance color map when source is Dashboard, otherwise uses chartLabelsColorMap', () => {
|
||||
const sliceId = 123;
|
||||
const colorScheme = 'preset';
|
||||
|
||||
// Mock chartLabelsColorMap and labelsColorMapInstance's getColorMap
|
||||
const chartColorMap = new Map([['testValueChart', 'chartColor']]);
|
||||
const dashboardColorMap = new Map([['testValueDash', 'dashboardColor']]);
|
||||
scale.chartLabelsColorMap = chartColorMap;
|
||||
jest
|
||||
.spyOn(scale.labelsColorMapInstance, 'getColorMap')
|
||||
.mockReturnValue(dashboardColorMap);
|
||||
|
||||
// Test when source is Dashboard
|
||||
scale.labelsColorMapInstance.source = LabelsColorMapSource.Dashboard;
|
||||
const colorFromDashboard = scale.getColor(
|
||||
'testValueDash',
|
||||
sliceId,
|
||||
colorScheme,
|
||||
);
|
||||
expect(colorFromDashboard).toBe('dashboardColor');
|
||||
|
||||
// Test when source is not Dashboard
|
||||
scale.labelsColorMapInstance.source = LabelsColorMapSource.Explore;
|
||||
const colorFromChart = scale.getColor(
|
||||
'testValueChart',
|
||||
sliceId,
|
||||
colorScheme,
|
||||
);
|
||||
expect(colorFromChart).toBe('chartColor');
|
||||
});
|
||||
it('returns same color for same value', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green'], {
|
||||
pig: 'red',
|
||||
@@ -177,7 +210,10 @@ describe('CategoricalColorScale', () => {
|
||||
scale.getColor('testValue3');
|
||||
scale.getColor('testValue4');
|
||||
|
||||
expect(getNextAvailableColorSpy).toHaveBeenCalledWith('blue');
|
||||
expect(getNextAvailableColorSpy).toHaveBeenCalledWith(
|
||||
'testValue4',
|
||||
'blue',
|
||||
);
|
||||
|
||||
getNextAvailableColorSpy.mockClear();
|
||||
|
||||
@@ -289,23 +325,25 @@ describe('CategoricalColorScale', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getNextAvailableColor(currentColor)', () => {
|
||||
describe('.getNextAvailableColor(currentLabel, currentColor)', () => {
|
||||
it('returns the current color if it is the least used or equally used among colors', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat');
|
||||
scale.getColor('dog');
|
||||
|
||||
// Since 'green' hasn't been used, it's considered the least used.
|
||||
expect(scale.getNextAvailableColor('blue')).toBe('green');
|
||||
expect(scale.getNextAvailableColor('fish', 'blue')).toBe('green');
|
||||
});
|
||||
|
||||
it('handles cases where all colors are equally used and returns the current color', () => {
|
||||
it('returns the least used color among all', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
scale.getColor('fish'); // green
|
||||
// All colors used once, so the function should return the current color
|
||||
expect(scale.getNextAvailableColor('red')).toBe('red');
|
||||
scale.getColor('puppy'); // blue
|
||||
scale.getColor('teddy'); // red
|
||||
// All colors used, so the function should return least used
|
||||
expect(scale.getNextAvailableColor('darling', 'red')).toBe('green');
|
||||
});
|
||||
|
||||
it('returns the least used color accurately even when some colors are used more frequently', () => {
|
||||
@@ -324,7 +362,57 @@ describe('CategoricalColorScale', () => {
|
||||
scale.getColor('pony'); // green
|
||||
|
||||
// Yellow is the least used color, so it should be returned.
|
||||
expect(scale.getNextAvailableColor('blue')).toBe('yellow');
|
||||
expect(scale.getNextAvailableColor('pony', 'blue')).toBe('yellow');
|
||||
});
|
||||
it('does not return adjacent colors if a non-adjacent color is equally used', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.chartLabelsColorMap.set('label1', 'red'); // Adjacent
|
||||
scale.chartLabelsColorMap.set('label2', 'blue'); // currentLabel
|
||||
scale.chartLabelsColorMap.set('label3', 'green'); // Adjacent
|
||||
|
||||
// Green and blue are equally used, but green is adjacent and penalized.
|
||||
expect(scale.getNextAvailableColor('label2', 'blue')).toBe('blue');
|
||||
});
|
||||
it('prioritizes a color that has never been used, even if there are adjacent colors', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
|
||||
scale.chartLabelsColorMap.set('label1', 'red');
|
||||
scale.chartLabelsColorMap.set('label2', 'blue'); // currentLabel
|
||||
|
||||
// Green has never been used, so it is prioritized.
|
||||
expect(scale.getNextAvailableColor('label2', 'blue')).toBe('green');
|
||||
});
|
||||
it('returns the least used or unused color when there are no adjacent labels', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
|
||||
// No adjacent labels are defined in chartLabelsColorMap.
|
||||
expect(scale.getNextAvailableColor('label2', 'green')).toBe('green');
|
||||
});
|
||||
it('handles colors that have never been used (fallback to usage count 0)', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
|
||||
// Do not use "green" at all
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
|
||||
// "green" has never been used, so usageCount for "green" should fallback to 0
|
||||
expect(scale.getNextAvailableColor('label2', 'red')).toBe('green');
|
||||
});
|
||||
it('handles a color with an explicit usage count of 0', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
|
||||
// Mock or override getColorUsageCount to return 0 for "blue"
|
||||
jest.spyOn(scale, 'getColorUsageCount').mockImplementation(color => {
|
||||
if (color === 'blue') return 0; // Explicitly return 0 for "blue"
|
||||
return 1; // Return 1 for other colors
|
||||
});
|
||||
|
||||
// "blue" should still be a valid option with a usage count of 0
|
||||
expect(scale.getNextAvailableColor('label1', 'red')).toBe('blue');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('LabelsColorMap', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
getLabelsColorMap().source = LabelsColorMapSource.Dashboard;
|
||||
getLabelsColorMap().clear();
|
||||
getLabelsColorMap().reset();
|
||||
});
|
||||
|
||||
it('has default value out-of-the-box', () => {
|
||||
@@ -92,11 +92,17 @@ describe('LabelsColorMap', () => {
|
||||
expect(Object.fromEntries(colorMap)).toEqual({ b: 'green' });
|
||||
});
|
||||
|
||||
it('should do nothing when source is not dashboard', () => {
|
||||
it('should set a new color only when source is dashboard', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Explore;
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
expect(Object.fromEntries(labelsColorMap.chartsLabelsMap)).toEqual({});
|
||||
const colorMap = labelsColorMap.getColorMap();
|
||||
expect(Object.fromEntries(colorMap)).toEqual({});
|
||||
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
const colorMap2 = labelsColorMap.getColorMap();
|
||||
expect(Object.fromEntries(colorMap2)).toEqual({ a: 'red' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,7 +132,7 @@ describe('LabelsColorMap', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.updateColorMap(namespace, scheme)', () => {
|
||||
describe('.updateColorMap(namespace, scheme, merge)', () => {
|
||||
let categoricalNamespace: any;
|
||||
let mockedNamespace: any;
|
||||
let labelsColorMap: any;
|
||||
@@ -141,18 +147,24 @@ describe('LabelsColorMap', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('should clear color map when not merge', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2', false);
|
||||
expect(labelsColorMap.colorMap).toEqual(new Map([['a', 'mockColor']]));
|
||||
});
|
||||
|
||||
it('should not clear color map when merge', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2', true);
|
||||
expect(labelsColorMap.colorMap).not.toEqual(new Map());
|
||||
});
|
||||
|
||||
it('should use provided color scheme', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2');
|
||||
expect(mockedNamespace.getScale).toHaveBeenCalledWith('testColors2');
|
||||
});
|
||||
|
||||
it('should fallback to original chart color scheme if no color scheme is provided', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1, 'originalScheme');
|
||||
labelsColorMap.updateColorMap(mockedNamespace);
|
||||
expect(mockedNamespace.getScale).toHaveBeenCalledWith('originalScheme');
|
||||
});
|
||||
|
||||
it('should fallback to undefined if no color scheme is provided', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.addSlice('b', 'blue', 2);
|
||||
@@ -181,6 +193,23 @@ describe('LabelsColorMap', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should update only new labels in the color map when merge', () => {
|
||||
labelsColorMap.colorMap = new Map();
|
||||
|
||||
labelsColorMap.addSlice('a', 'yellow', 1);
|
||||
labelsColorMap.addSlice('b', 'green', 1);
|
||||
labelsColorMap.addSlice('c', 'purple', 1);
|
||||
|
||||
labelsColorMap.updateColorMap(categoricalNamespace, 'testColors2', true);
|
||||
|
||||
const mergedColorMap = labelsColorMap.getColorMap();
|
||||
expect(Object.fromEntries(mergedColorMap)).toEqual({
|
||||
a: 'yellow',
|
||||
b: 'green',
|
||||
c: 'purple',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use recycle colors', () => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.UseAnalagousColors]: false,
|
||||
@@ -231,4 +260,47 @@ describe('LabelsColorMap', () => {
|
||||
expect(Object.fromEntries(colorMap)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOwnColorScheme(sliceId, ownScheme)', () => {
|
||||
it('should update the scheme in the config', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Explore;
|
||||
const sliceId = 1;
|
||||
const initialConfig = { labels: ['initial config'] };
|
||||
|
||||
labelsColorMap.chartsLabelsMap = new Map();
|
||||
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
|
||||
|
||||
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
|
||||
|
||||
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
|
||||
...initialConfig,
|
||||
ownScheme: 'newScheme',
|
||||
});
|
||||
});
|
||||
it('should update ownScheme when source is not Explore', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
const sliceId = 1;
|
||||
const initialConfig = { labels: ['initial config'] };
|
||||
|
||||
labelsColorMap.chartsLabelsMap = new Map();
|
||||
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
|
||||
|
||||
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
|
||||
|
||||
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
|
||||
...initialConfig,
|
||||
ownScheme: 'newScheme',
|
||||
});
|
||||
});
|
||||
it('should do nothing when chart config does not exist', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Explore;
|
||||
const sliceId = 1;
|
||||
|
||||
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
|
||||
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ function Chord(element, props) {
|
||||
.append('path')
|
||||
.attr('id', (d, i) => `group${i}`)
|
||||
.attr('d', arc)
|
||||
.style('fill', (d, i) => colorFn(nodes[i], sliceId, colorScheme));
|
||||
.style('fill', (d, i) => colorFn(nodes[i], sliceId));
|
||||
|
||||
// Add a text label.
|
||||
const groupText = group.append('text').attr('x', 6).attr('dy', 15);
|
||||
@@ -121,7 +121,7 @@ function Chord(element, props) {
|
||||
.on('mouseover', d => {
|
||||
chord.classed('fade', p => p !== d);
|
||||
})
|
||||
.style('fill', d => colorFn(nodes[d.source.index], sliceId, colorScheme))
|
||||
.style('fill', d => colorFn(nodes[d.source.index], sliceId))
|
||||
.attr('d', path);
|
||||
|
||||
// Add an elaborate mouseover title for each chord.
|
||||
|
||||
@@ -37,6 +37,7 @@ const propTypes = {
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
country: PropTypes.string,
|
||||
colorScheme: PropTypes.string,
|
||||
linearColorScheme: PropTypes.string,
|
||||
mapBaseUrl: PropTypes.string,
|
||||
numberFormat: PropTypes.string,
|
||||
|
||||
@@ -73,12 +73,11 @@ class CustomHistogram extends PureComponent {
|
||||
showLegend,
|
||||
sliceId,
|
||||
} = this.props;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const keys = data.map(d => d.key);
|
||||
const colorScale = scaleOrdinal({
|
||||
domain: keys,
|
||||
range: keys.map(x => colorFn(x, sliceId, colorScheme)),
|
||||
range: keys.map(x => colorFn(x, sliceId)),
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -384,7 +384,7 @@ function Icicle(element, props) {
|
||||
|
||||
// Apply color scheme
|
||||
g.selectAll('rect').style('fill', d => {
|
||||
d.color = colorFn(d.name, sliceId, colorScheme);
|
||||
d.color = colorFn(d.name, sliceId);
|
||||
|
||||
return d.color;
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ const propTypes = {
|
||||
numberFormat: PropTypes.string,
|
||||
useRichTooltip: PropTypes.bool,
|
||||
useAreaProportions: PropTypes.bool,
|
||||
colorScheme: PropTypes.string,
|
||||
};
|
||||
|
||||
function copyArc(d) {
|
||||
@@ -120,14 +121,14 @@ function Rose(element, props) {
|
||||
.map(v => ({
|
||||
key: v.name,
|
||||
value: v.value,
|
||||
color: colorFn(v.name, sliceId, colorScheme),
|
||||
color: colorFn(v.name, sliceId),
|
||||
highlight: v.id === d.arcId,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: d.name,
|
||||
value: d.val,
|
||||
color: colorFn(d.name, sliceId, colorScheme),
|
||||
color: colorFn(d.name, sliceId),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -138,7 +139,7 @@ function Rose(element, props) {
|
||||
};
|
||||
}
|
||||
|
||||
legend.width(width).color(d => colorFn(d.key, sliceId, colorScheme));
|
||||
legend.width(width).color(d => colorFn(d.key, sliceId));
|
||||
legendWrap.datum(legendData(datum)).call(legend);
|
||||
|
||||
tooltip.headerFormatter(timeFormat).valueFormatter(format);
|
||||
@@ -385,7 +386,7 @@ function Rose(element, props) {
|
||||
const arcs = ae
|
||||
.append('path')
|
||||
.attr('class', 'arc')
|
||||
.attr('fill', d => colorFn(d.name, sliceId, colorScheme))
|
||||
.attr('fill', d => colorFn(d.name, sliceId))
|
||||
.attr('d', arc);
|
||||
|
||||
function mousemove() {
|
||||
|
||||
@@ -83,7 +83,7 @@ function computeGraph(links) {
|
||||
|
||||
function SankeyLoop(element, props) {
|
||||
const { data, width, height, colorScheme, sliceId } = props;
|
||||
const color = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const margin = { ...defaultMargin, ...props.margin };
|
||||
const innerWidth = width - margin.left - margin.right;
|
||||
const innerHeight = height - margin.top - margin.bottom;
|
||||
@@ -107,7 +107,7 @@ function SankeyLoop(element, props) {
|
||||
value / sValue,
|
||||
)})`,
|
||||
)
|
||||
.linkColor(d => color(d.source.name, sliceId));
|
||||
.linkColor(d => colorFn(d.source.name, sliceId));
|
||||
|
||||
const div = select(element);
|
||||
div.selectAll('*').remove();
|
||||
|
||||
@@ -67,7 +67,6 @@ function Sankey(element, props) {
|
||||
.attr('height', innerHeight + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
const sankey = d3Sankey()
|
||||
@@ -219,7 +218,7 @@ function Sankey(element, props) {
|
||||
.attr('width', sankey.nodeWidth())
|
||||
.style('fill', d => {
|
||||
const name = d.name || 'N/A';
|
||||
d.color = colorFn(name, sliceId, colorScheme);
|
||||
d.color = colorFn(name, sliceId);
|
||||
|
||||
return d.color;
|
||||
})
|
||||
|
||||
@@ -43,6 +43,7 @@ const propTypes = {
|
||||
showBubbles: PropTypes.bool,
|
||||
linearColorScheme: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
colorScheme: PropTypes.string,
|
||||
setDataMask: PropTypes.func,
|
||||
onContextMenu: PropTypes.func,
|
||||
emitCrossFilters: PropTypes.bool,
|
||||
@@ -85,24 +86,24 @@ function WorldMap(element, props) {
|
||||
.range([1, maxBubbleSize]);
|
||||
|
||||
let processedData;
|
||||
let colorScale;
|
||||
let colorFn;
|
||||
if (colorBy === ColorBy.Country) {
|
||||
colorScale = CategoricalColorNamespace.getScale(colorScheme);
|
||||
colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
processedData = filteredData.map(d => ({
|
||||
...d,
|
||||
radius: radiusScale(Math.sqrt(d.m2)),
|
||||
fillColor: colorScale(d.name, sliceId),
|
||||
fillColor: colorFn(d.name, sliceId),
|
||||
}));
|
||||
} else {
|
||||
colorScale = getSequentialSchemeRegistry()
|
||||
colorFn = getSequentialSchemeRegistry()
|
||||
.get(linearColorScheme)
|
||||
.createLinearScale(d3Extent(filteredData, d => d.m1));
|
||||
|
||||
processedData = filteredData.map(d => ({
|
||||
...d,
|
||||
radius: radiusScale(Math.sqrt(d.m2)),
|
||||
fillColor: colorScale(d.m1),
|
||||
fillColor: colorFn(d.m1),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -52,16 +52,14 @@ const { getScale } = CategoricalColorNamespace;
|
||||
function getCategories(fd: QueryFormData, data: JsonObject[]) {
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||
const colorFn = getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorFn = getScale(appliedScheme);
|
||||
const categories = {};
|
||||
data.forEach(d => {
|
||||
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
color = hexToRGB(
|
||||
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
|
||||
c.a * 255,
|
||||
);
|
||||
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
|
||||
} else {
|
||||
color = fixedColor;
|
||||
}
|
||||
@@ -132,15 +130,13 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
|
||||
const addColor = useCallback((data: JsonObject[], fd: QueryFormData) => {
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const colorFn = getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorFn = getScale(appliedScheme);
|
||||
|
||||
return data.map(d => {
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
color = hexToRGB(
|
||||
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
|
||||
c.a * 255,
|
||||
);
|
||||
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
|
||||
|
||||
return { ...d, color };
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ export function getLayer(
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
const fd = formData;
|
||||
const colorScale = CategoricalColorNamespace.getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
const colorRange = colorScale
|
||||
.range()
|
||||
.map(color => hexToRGB(color)) as Color[];
|
||||
|
||||
@@ -54,7 +54,8 @@ export function getLayer(
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
const fd = formData;
|
||||
const colorScale = CategoricalColorNamespace.getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
const colorRange = colorScale
|
||||
.range()
|
||||
.map(color => hexToRGB(color)) as Color[];
|
||||
|
||||
@@ -658,9 +658,7 @@ function nvd3Vis(element, props) {
|
||||
} else if (vizType !== 'bullet') {
|
||||
const colorFn = getScale(colorScheme);
|
||||
chart.color(
|
||||
d =>
|
||||
d.color ||
|
||||
colorFn(cleanColorInput(d[colorKey]), sliceId, colorScheme),
|
||||
d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,9 +109,9 @@ export default function transformProps(
|
||||
datum[`${metric}__outliers`],
|
||||
],
|
||||
itemStyle: {
|
||||
color: colorFn(groupbyLabel, sliceId, colorScheme),
|
||||
color: colorFn(groupbyLabel, sliceId),
|
||||
opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6,
|
||||
borderColor: colorFn(groupbyLabel, sliceId, colorScheme),
|
||||
borderColor: colorFn(groupbyLabel, sliceId),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -150,7 +150,7 @@ export default function transformProps(
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
color: colorFn(groupbyLabel, sliceId, colorScheme),
|
||||
color: colorFn(groupbyLabel, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -108,8 +108,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
|
||||
legendOrientation,
|
||||
legendMargin,
|
||||
legendType,
|
||||
sliceId,
|
||||
}: EchartsBubbleFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
|
||||
const legends = new Set<string>();
|
||||
@@ -138,7 +138,10 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
|
||||
],
|
||||
],
|
||||
type: 'scatter',
|
||||
itemStyle: { color: colorFn(name), opacity },
|
||||
itemStyle: {
|
||||
color: colorFn(name, sliceId),
|
||||
opacity,
|
||||
},
|
||||
});
|
||||
legends.add(name);
|
||||
});
|
||||
|
||||
@@ -145,7 +145,6 @@ export default function transformProps(
|
||||
}, {});
|
||||
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getValueFormatter(
|
||||
metric,
|
||||
@@ -175,7 +174,7 @@ export default function transformProps(
|
||||
value,
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(name, sliceId, colorScheme),
|
||||
color: colorFn(name, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -166,6 +166,7 @@ export default function transformProps(
|
||||
const name = groupbyLabels
|
||||
.map(column => `${verboseMap[column] || column}: ${data_point[column]}`)
|
||||
.join(', ');
|
||||
const colorLabel = groupbyLabels.map(col => data_point[col] as string);
|
||||
columnsLabelMap.set(
|
||||
name,
|
||||
groupbyLabels.map(col => data_point[col] as string),
|
||||
@@ -174,7 +175,7 @@ export default function transformProps(
|
||||
value: data_point[metricLabel] as number,
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(index, sliceId, colorScheme),
|
||||
color: colorFn(colorLabel, sliceId),
|
||||
},
|
||||
title: {
|
||||
offsetCenter: [
|
||||
@@ -202,7 +203,7 @@ export default function transformProps(
|
||||
item = {
|
||||
...item,
|
||||
itemStyle: {
|
||||
color: colorFn(index, sliceId, colorScheme),
|
||||
color: colorFn(index, sliceId),
|
||||
opacity: OpacityEnum.SemiTransparent,
|
||||
},
|
||||
detail: {
|
||||
|
||||
@@ -198,6 +198,7 @@ export default function transformProps(
|
||||
const refs: Refs = {};
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const firstColor = colorFn.range()[0];
|
||||
const nodes: { [name: string]: number } = {};
|
||||
const categories: Set<string> = new Set();
|
||||
const echartNodes: EChartGraphNode[] = [];
|
||||
@@ -207,7 +208,12 @@ export default function transformProps(
|
||||
* Get the node id of an existing node,
|
||||
* or create a new node if it doesn't exist.
|
||||
*/
|
||||
function getOrCreateNode(name: string, col: string, category?: string) {
|
||||
function getOrCreateNode(
|
||||
name: string,
|
||||
col: string,
|
||||
category?: string,
|
||||
color?: string,
|
||||
) {
|
||||
if (!(name in nodes)) {
|
||||
nodes[name] = echartNodes.length;
|
||||
echartNodes.push({
|
||||
@@ -221,6 +227,7 @@ export default function transformProps(
|
||||
...getDefaultTooltip(refs),
|
||||
...DEFAULT_GRAPH_SERIES_OPTION.tooltip,
|
||||
},
|
||||
itemStyle: { color },
|
||||
});
|
||||
}
|
||||
const node = echartNodes[nodes[name]];
|
||||
@@ -248,8 +255,25 @@ export default function transformProps(
|
||||
const targetCategoryName = targetCategory
|
||||
? getCategoryName(targetCategory, link[targetCategory])
|
||||
: undefined;
|
||||
const sourceNode = getOrCreateNode(sourceName, source, sourceCategoryName);
|
||||
const targetNode = getOrCreateNode(targetName, target, targetCategoryName);
|
||||
const sourceNodeColor = sourceCategoryName
|
||||
? colorFn(sourceCategoryName)
|
||||
: firstColor;
|
||||
const targetNodeColor = targetCategoryName
|
||||
? colorFn(targetCategoryName)
|
||||
: firstColor;
|
||||
|
||||
const sourceNode = getOrCreateNode(
|
||||
sourceName,
|
||||
source,
|
||||
sourceCategoryName,
|
||||
sourceNodeColor,
|
||||
);
|
||||
const targetNode = getOrCreateNode(
|
||||
targetName,
|
||||
target,
|
||||
targetCategoryName,
|
||||
targetNodeColor,
|
||||
);
|
||||
|
||||
sourceNode.value += value;
|
||||
targetNode.value += value;
|
||||
@@ -258,7 +282,9 @@ export default function transformProps(
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id,
|
||||
value,
|
||||
lineStyle: {},
|
||||
lineStyle: {
|
||||
color: sourceNodeColor,
|
||||
},
|
||||
emphasis: {},
|
||||
select: {},
|
||||
});
|
||||
@@ -271,14 +297,15 @@ export default function transformProps(
|
||||
});
|
||||
|
||||
const categoryList = [...categories];
|
||||
|
||||
const series: GraphSeriesOption[] = [
|
||||
{
|
||||
zoom: DEFAULT_GRAPH_SERIES_OPTION.zoom,
|
||||
type: 'graph',
|
||||
categories: categoryList.map(c => ({
|
||||
name: c,
|
||||
itemStyle: { color: colorFn(c, sliceId, colorScheme) },
|
||||
itemStyle: {
|
||||
color: colorFn(c, sliceId),
|
||||
},
|
||||
})),
|
||||
layout,
|
||||
force: {
|
||||
|
||||
@@ -191,7 +191,6 @@ export default function transformProps(
|
||||
}, {});
|
||||
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getValueFormatter(
|
||||
metric,
|
||||
@@ -223,7 +222,7 @@ export default function transformProps(
|
||||
value,
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(name, sliceId, colorScheme),
|
||||
color: colorFn(name, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -109,7 +109,6 @@ export default function transformProps(
|
||||
...formData,
|
||||
};
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getNumberFormatter(numberFormat);
|
||||
const formatter = (params: CallbackDataParams) =>
|
||||
@@ -166,7 +165,7 @@ export default function transformProps(
|
||||
value: metricLabels.map(metricLabel => datum[metricLabel]),
|
||||
name: joinedName,
|
||||
itemStyle: {
|
||||
color: colorFn(joinedName, sliceId, colorScheme),
|
||||
color: colorFn(joinedName, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.Transparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function transformProps(
|
||||
const refs: Refs = {};
|
||||
const { formData, height, hooks, queriesData, width } = chartProps;
|
||||
const { onLegendStateChanged } = hooks;
|
||||
const { colorScheme, metric, source, target } = formData;
|
||||
const { colorScheme, metric, source, target, sliceId } = formData;
|
||||
const { data } = queriesData[0];
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
@@ -68,7 +68,7 @@ export default function transformProps(
|
||||
).map(name => ({
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(name),
|
||||
color: colorFn(name, sliceId),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
SupersetThemeProps,
|
||||
withTheme,
|
||||
seed,
|
||||
CategoricalColorScale,
|
||||
CategoricalColorNamespace,
|
||||
} from '@superset-ui/core';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@@ -230,7 +230,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
encoder.channels.color.setDomainFromDataset(words);
|
||||
|
||||
const { getValueFromDatum } = encoder.channels.color;
|
||||
const colorFn = encoder.channels.color.scale as CategoricalColorScale;
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
const viewBoxWidth = width * scaleFactor;
|
||||
const viewBoxHeight = height * scaleFactor;
|
||||
@@ -250,11 +250,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
fontSize={`${w.size}px`}
|
||||
fontWeight={w.weight}
|
||||
fontFamily={w.font}
|
||||
fill={colorFn(
|
||||
getValueFromDatum(w) as string,
|
||||
sliceId,
|
||||
colorScheme,
|
||||
)}
|
||||
fill={colorFn(getValueFromDatum(w) as string, sliceId)}
|
||||
textAnchor="middle"
|
||||
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
|
||||
>
|
||||
|
||||
@@ -26,10 +26,7 @@ export const getDashboardFormData = (overrides: JsonObject = {}) => ({
|
||||
girl: '#FF69B4',
|
||||
boy: '#ADD8E6',
|
||||
},
|
||||
shared_label_colors: {
|
||||
boy: '#ADD8E6',
|
||||
girl: '#FF69B4',
|
||||
},
|
||||
shared_label_colors: ['boy', 'girl'],
|
||||
color_scheme: 'd3Category20b',
|
||||
extra_filters: [
|
||||
{
|
||||
|
||||
@@ -113,6 +113,6 @@ export const overwriteConfirmMetadata = {
|
||||
slug: null,
|
||||
owners: [],
|
||||
json_metadata:
|
||||
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":{"Male":"#5ac19e","Female":"#1f86c9","<NULL>":"#5AC189","Prefer not to say":"#47457c","No Answer":"#e05043","Yes, an ethnic minority":"#666666","No, not an ethnic minority":"#ffa444","age":"#1FA8C9"},"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
|
||||
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":["Male", "Female","<NULL>","Prefer not to say","No Answer","Yes, an ethnic minority","No, not an ethnic minority","age"],"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,15 +16,58 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import Slider, { SliderSingleProps } from '.';
|
||||
import Slider, { SliderSingleProps, SliderRangeProps } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Slider',
|
||||
component: Slider,
|
||||
};
|
||||
|
||||
export const InteractiveSlider = (args: SliderSingleProps) => (
|
||||
<Slider {...args} style={{ width: 400, height: 400 }} />
|
||||
const tooltipPlacement = [
|
||||
'top',
|
||||
'left',
|
||||
'bottom',
|
||||
'right',
|
||||
'topLeft',
|
||||
'topRight',
|
||||
'bottomLeft',
|
||||
'bottomRight',
|
||||
'leftTop',
|
||||
'leftBottom',
|
||||
'rightTop',
|
||||
'rightBottom',
|
||||
] as const;
|
||||
|
||||
export const InteractiveSlider = ({
|
||||
tooltipOpen,
|
||||
tooltipPosition,
|
||||
...args
|
||||
}: SliderSingleProps & {
|
||||
tooltipOpen: boolean;
|
||||
tooltipPosition: (typeof tooltipPlacement)[number];
|
||||
}) => (
|
||||
<Slider
|
||||
{...args}
|
||||
tooltip={{
|
||||
...args.tooltip,
|
||||
open: tooltipOpen,
|
||||
placement: tooltipPosition,
|
||||
}}
|
||||
style={{ width: 400, height: 400 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const InteractiveRangeSlider = ({
|
||||
tooltipOpen,
|
||||
draggableTrack,
|
||||
...args
|
||||
}: SliderRangeProps & { tooltipOpen: boolean; draggableTrack: boolean }) => (
|
||||
<Slider
|
||||
{...args}
|
||||
tooltip={{ open: tooltipOpen }}
|
||||
range={{ draggableTrack }}
|
||||
style={{ width: 400, height: 400 }}
|
||||
/>
|
||||
);
|
||||
|
||||
InteractiveSlider.args = {
|
||||
@@ -32,17 +75,33 @@ InteractiveSlider.args = {
|
||||
max: 100,
|
||||
defaultValue: 70,
|
||||
step: 1,
|
||||
marks: {},
|
||||
disabled: false,
|
||||
reverse: false,
|
||||
vertical: false,
|
||||
autoFocus: false,
|
||||
keyboard: true,
|
||||
dots: false,
|
||||
included: true,
|
||||
tooltipPosition: 'bottom',
|
||||
};
|
||||
|
||||
InteractiveSlider.argTypes = {
|
||||
onChange: { action: 'onChange' },
|
||||
disabled: {
|
||||
onChangeComplete: { action: 'onChangeComplete' },
|
||||
tooltipOpen: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
reverse: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
vertical: {
|
||||
control: { type: 'boolean' },
|
||||
tooltipPosition: {
|
||||
options: tooltipPlacement,
|
||||
control: { type: 'select' },
|
||||
},
|
||||
};
|
||||
|
||||
InteractiveRangeSlider.args = {
|
||||
...InteractiveSlider.args,
|
||||
defaultValue: [50, 70],
|
||||
draggableTrack: false,
|
||||
};
|
||||
|
||||
InteractiveRangeSlider.argTypes = InteractiveSlider.argTypes;
|
||||
|
||||
39
superset-frontend/src/components/Slider/Slider.test.tsx
Normal file
39
superset-frontend/src/components/Slider/Slider.test.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import Slider from '.';
|
||||
|
||||
const mockedProps = {
|
||||
defaultValue: 90,
|
||||
tooltip: {
|
||||
open: true,
|
||||
},
|
||||
};
|
||||
|
||||
test('should render', () => {
|
||||
const { container } = render(<Slider {...mockedProps} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render with default value on tooltip', () => {
|
||||
render(<Slider {...mockedProps} />);
|
||||
expect(
|
||||
screen.getAllByText(`${mockedProps.defaultValue}`)[0],
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import AntdSlider, {
|
||||
SliderSingleProps,
|
||||
SliderRangeProps,
|
||||
} from 'antd/lib/slider';
|
||||
import { SliderSingleProps, SliderRangeProps } from 'antd-v5/lib/slider';
|
||||
import { Slider as AntdSlider } from 'antd-v5';
|
||||
|
||||
export type { SliderSingleProps, SliderRangeProps };
|
||||
|
||||
export default function Slider(props: SliderSingleProps | SliderRangeProps) {
|
||||
return <AntdSlider {...props} css={{ marginLeft: 0, marginRight: 0 }} />;
|
||||
return <AntdSlider {...props} />;
|
||||
}
|
||||
|
||||
@@ -30,10 +30,13 @@ const loadComponent = (mockCurrentTime?: string) => {
|
||||
return new Promise<FC<TimezoneSelectorProps>>(resolve => {
|
||||
const { default: TimezoneSelector } = module.require('./index');
|
||||
resolve(TimezoneSelector);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('render timezones in correct order for daylight saving time', async () => {
|
||||
const TimezoneSelector = await loadComponent('2022-07-01');
|
||||
const onTimezoneChange = jest.fn();
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import moment from 'moment-timezone';
|
||||
import { FC } from 'react';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { TimezoneSelectorProps } from './index';
|
||||
|
||||
const loadComponent = (mockCurrentTime?: string) => {
|
||||
@@ -30,7 +30,6 @@ const loadComponent = (mockCurrentTime?: string) => {
|
||||
return new Promise<FC<TimezoneSelectorProps>>(resolve => {
|
||||
const { default: TimezoneSelector } = module.require('./index');
|
||||
resolve(TimezoneSelector);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,6 +43,10 @@ const openSelectMenu = () => {
|
||||
|
||||
jest.spyOn(moment.tz, 'guess').mockReturnValue('America/New_York');
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('use the timezone from `moment` if no timezone provided', async () => {
|
||||
const TimezoneSelector = await loadComponent('2022-01-01');
|
||||
const onTimezoneChange = jest.fn();
|
||||
@@ -89,7 +92,6 @@ test('render timezones in correct oder for standard time', async () => {
|
||||
expect(options[0]).toHaveTextContent('GMT -05:00 (Eastern Standard Time)');
|
||||
expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Pago_Pago)');
|
||||
expect(options[2]).toHaveTextContent('GMT -10:00 (Hawaii Standard Time)');
|
||||
expect(options[3]).toHaveTextContent('GMT -10:00 (America/Adak)');
|
||||
});
|
||||
|
||||
test('can select a timezone values and returns canonical timezone name', async () => {
|
||||
@@ -106,7 +108,7 @@ test('can select a timezone values and returns canonical timezone name', async (
|
||||
|
||||
const searchInput = screen.getByRole('combobox');
|
||||
// search for mountain time
|
||||
await userEvent.type(searchInput, 'mou', { delay: 10 });
|
||||
await userEvent.type(searchInput, 'mou');
|
||||
const findTitle = 'GMT -07:00 (Mountain Standard Time)';
|
||||
const selectOption = await screen.findByTitle(findTitle);
|
||||
userEvent.click(selectOption);
|
||||
|
||||
@@ -55,6 +55,7 @@ import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import { safeStringify } from 'src/utils/safeStringify';
|
||||
import { logEvent } from 'src/logger/actions';
|
||||
import { LOG_ACTIONS_CONFIRM_OVERWRITE_DASHBOARD_METADATA } from 'src/logger/LogUtils';
|
||||
import { isEqual } from 'lodash';
|
||||
import { UPDATE_COMPONENTS_PARENTS_LIST } from './dashboardLayout';
|
||||
import {
|
||||
saveChartConfiguration,
|
||||
@@ -68,9 +69,10 @@ import getOverwriteItems from '../util/getOverwriteItems';
|
||||
import {
|
||||
applyColors,
|
||||
isLabelsColorMapSynced,
|
||||
getLabelsColorMapEntries,
|
||||
getColorSchemeDomain,
|
||||
getColorNamespace,
|
||||
getLabelsColorMapEntries,
|
||||
getFreshSharedLabels,
|
||||
} from '../../utils/colorScheme';
|
||||
|
||||
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
|
||||
@@ -224,6 +226,41 @@ export function saveDashboardFinished() {
|
||||
return { type: SAVE_DASHBOARD_FINISHED };
|
||||
}
|
||||
|
||||
export const SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE =
|
||||
'SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE';
|
||||
export const SET_DASHBOARD_LABELS_COLORMAP_SYNCED =
|
||||
'SET_DASHBOARD_LABELS_COLORMAP_SYNCED';
|
||||
export const SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE =
|
||||
'SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE';
|
||||
export const SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED =
|
||||
'SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED';
|
||||
|
||||
export function setDashboardLabelsColorMapSync() {
|
||||
return { type: SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE };
|
||||
}
|
||||
|
||||
export function setDashboardLabelsColorMapSynced() {
|
||||
return { type: SET_DASHBOARD_LABELS_COLORMAP_SYNCED };
|
||||
}
|
||||
|
||||
export function setDashboardSharedLabelsColorsSync() {
|
||||
return { type: SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE };
|
||||
}
|
||||
|
||||
export function setDashboardSharedLabelsColorsSynced() {
|
||||
return { type: SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED };
|
||||
}
|
||||
|
||||
export const setDashboardMetadata = updatedMetadata => async dispatch => {
|
||||
dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata: {
|
||||
...updatedMetadata,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export function saveDashboardRequest(data, id, saveType) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: UPDATE_COMPONENTS_PARENTS_LIST });
|
||||
@@ -254,6 +291,9 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
|
||||
const hasId = item => item.id !== undefined;
|
||||
const metadataCrossFiltersEnabled = data.metadata?.cross_filters_enabled;
|
||||
const colorScheme = data.metadata?.color_scheme;
|
||||
const customLabelsColor = data.metadata?.label_colors || {};
|
||||
const sharedLabelsColor = data.metadata?.shared_label_colors || [];
|
||||
// making sure the data is what the backend expects
|
||||
const cleanedData = {
|
||||
...data,
|
||||
@@ -270,11 +310,14 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
metadata: {
|
||||
...data.metadata,
|
||||
color_namespace: getColorNamespace(data.metadata?.color_namespace),
|
||||
color_scheme: data.metadata?.color_scheme || '',
|
||||
color_scheme_domain: data.metadata?.color_scheme_domain || [],
|
||||
color_scheme: colorScheme || '',
|
||||
color_scheme_domain: colorScheme
|
||||
? getColorSchemeDomain(colorScheme)
|
||||
: [],
|
||||
expanded_slices: data.metadata?.expanded_slices || {},
|
||||
label_colors: data.metadata?.label_colors || {},
|
||||
shared_label_colors: data.metadata?.shared_label_colors || {},
|
||||
label_colors: customLabelsColor,
|
||||
shared_label_colors: getFreshSharedLabels(sharedLabelsColor),
|
||||
map_label_colors: getLabelsColorMapEntries(customLabelsColor),
|
||||
refresh_frequency: data.metadata?.refresh_frequency || 0,
|
||||
timed_refresh_immune_slices:
|
||||
data.metadata?.timed_refresh_immune_slices || [],
|
||||
@@ -324,11 +367,7 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
// syncing with the backend transformations of the metadata
|
||||
if (updatedDashboard.json_metadata) {
|
||||
const metadata = JSON.parse(updatedDashboard.json_metadata);
|
||||
dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata,
|
||||
}),
|
||||
);
|
||||
dispatch(setDashboardMetadata(metadata));
|
||||
if (metadata.chart_configuration) {
|
||||
dispatch({
|
||||
type: SAVE_CHART_CONFIG_COMPLETE,
|
||||
@@ -677,68 +716,261 @@ export function setDatasetsStatus(status) {
|
||||
};
|
||||
}
|
||||
|
||||
const updateDashboardMetadata = async (id, metadata, dispatch) => {
|
||||
await SupersetClient.put({
|
||||
const storeDashboardMetadata = async (id, metadata) =>
|
||||
SupersetClient.put({
|
||||
endpoint: `/api/v1/dashboard/${id}`,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ json_metadata: JSON.stringify(metadata) }),
|
||||
});
|
||||
dispatch(dashboardInfoChanged({ metadata }));
|
||||
};
|
||||
|
||||
export const updateDashboardLabelsColor = () => async (dispatch, getState) => {
|
||||
/**
|
||||
*
|
||||
* Persists the label colors maps in the dashboard metadata.
|
||||
* It runs when outdated color info are detected in stored metadata.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
export const persistDashboardLabelsColor = () => async (dispatch, getState) => {
|
||||
const {
|
||||
dashboardInfo: { id, metadata },
|
||||
dashboardState: { labelsColorMapMustSync, sharedLabelsColorsMustSync },
|
||||
} = getState();
|
||||
const categoricalSchemes = getCategoricalSchemeRegistry();
|
||||
const colorScheme = metadata?.color_scheme;
|
||||
const colorSchemeRegistry = categoricalSchemes.get(
|
||||
metadata?.color_scheme,
|
||||
true,
|
||||
);
|
||||
const defaultScheme = categoricalSchemes.defaultKey;
|
||||
const fallbackScheme = defaultScheme?.toString() || 'supersetColors';
|
||||
const colorSchemeDomain = metadata?.color_scheme_domain || [];
|
||||
|
||||
if (labelsColorMapMustSync || sharedLabelsColorsMustSync) {
|
||||
storeDashboardMetadata(id, metadata);
|
||||
dispatch(setDashboardLabelsColorMapSynced());
|
||||
dispatch(setDashboardSharedLabelsColorsSynced());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the stored dashboard metadata for inconsistencies.
|
||||
* Update the current metadata with validated color information.
|
||||
* It runs only on Dashboard page load.
|
||||
*
|
||||
* @param {*} metadata - the stored dashboard metadata
|
||||
* @returns void
|
||||
*/
|
||||
export const applyDashboardLabelsColorOnLoad = metadata => async dispatch => {
|
||||
try {
|
||||
const updatedMetadata = { ...metadata };
|
||||
let updatedScheme = metadata?.color_scheme;
|
||||
const customLabelsColor = metadata.label_colors || {};
|
||||
const sharedLabelsColor = metadata.shared_label_colors || [];
|
||||
let hasChanged = false;
|
||||
|
||||
// backward compatibility of shared_label_colors
|
||||
const sharedLabels = metadata.shared_label_colors || [];
|
||||
if (!Array.isArray(sharedLabels) && Object.keys(sharedLabels).length > 0) {
|
||||
hasChanged = true;
|
||||
updatedMetadata.shared_label_colors = getFreshSharedLabels(
|
||||
Object.keys(sharedLabelsColor),
|
||||
);
|
||||
}
|
||||
// backward compatibility of map_label_colors
|
||||
const hasMapLabelColors =
|
||||
Object.keys(metadata.map_label_colors || {}).length > 0;
|
||||
|
||||
let updatedScheme = metadata.color_scheme;
|
||||
const categoricalSchemes = getCategoricalSchemeRegistry();
|
||||
const colorSchemeRegistry = categoricalSchemes.get(updatedScheme, true);
|
||||
const hasInvalidColorScheme = !!updatedScheme && !colorSchemeRegistry;
|
||||
|
||||
// color scheme might not exist any longer
|
||||
if (hasInvalidColorScheme) {
|
||||
const defaultScheme = categoricalSchemes.defaultKey;
|
||||
const fallbackScheme = defaultScheme?.toString() || 'supersetColors';
|
||||
hasChanged = true;
|
||||
|
||||
// Color scheme does not exist anymore, fallback to default
|
||||
if (colorScheme && !colorSchemeRegistry) {
|
||||
updatedScheme = fallbackScheme;
|
||||
updatedMetadata.color_scheme = updatedScheme;
|
||||
updatedMetadata.color_scheme_domain = getColorSchemeDomain(colorScheme);
|
||||
|
||||
dispatch(setColorScheme(updatedScheme));
|
||||
// must re-apply colors from fresh labels color map
|
||||
applyColors(updatedMetadata, true);
|
||||
}
|
||||
|
||||
// stored labels color map and applied might differ
|
||||
const isMapSynced = isLabelsColorMapSynced(metadata);
|
||||
if (!isMapSynced) {
|
||||
// re-apply a fresh labels color map
|
||||
applyColors(updatedMetadata, true);
|
||||
// pull and store the just applied labels color map
|
||||
updatedMetadata.shared_label_colors = getLabelsColorMapEntries();
|
||||
}
|
||||
|
||||
// the stored color domain registry and fresh might differ at this point
|
||||
const freshColorSchemeDomain = getColorSchemeDomain(colorScheme);
|
||||
const isRegistrySynced =
|
||||
colorSchemeDomain.toString() !== freshColorSchemeDomain.toString();
|
||||
if (colorScheme && !isRegistrySynced) {
|
||||
const freshColorSchemeDomain = updatedScheme
|
||||
? getColorSchemeDomain(updatedScheme)
|
||||
: [];
|
||||
const currentColorSchemeDomain = metadata.color_scheme_domain || [];
|
||||
|
||||
if (!isEqual(freshColorSchemeDomain, currentColorSchemeDomain)) {
|
||||
hasChanged = true;
|
||||
updatedMetadata.color_scheme_domain = freshColorSchemeDomain;
|
||||
}
|
||||
|
||||
if (
|
||||
(colorScheme && (!colorSchemeRegistry || !isRegistrySynced)) ||
|
||||
!isMapSynced
|
||||
) {
|
||||
await updateDashboardMetadata(id, updatedMetadata, dispatch);
|
||||
// if color scheme is invalid or map is missing, apply a fresh color map
|
||||
// if valid, apply the stored map to keep consistency across refreshes
|
||||
const shouldGoFresh = !hasMapLabelColors || hasInvalidColorScheme;
|
||||
applyColors(updatedMetadata, shouldGoFresh);
|
||||
|
||||
if (shouldGoFresh) {
|
||||
// a fresh color map has been applied
|
||||
// needs to be stored for consistency
|
||||
hasChanged = true;
|
||||
updatedMetadata.map_label_colors =
|
||||
getLabelsColorMapEntries(customLabelsColor);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update dashboard color settings:', error);
|
||||
|
||||
if (hasChanged) {
|
||||
dispatch(setDashboardMetadata(updatedMetadata));
|
||||
dispatch(setDashboardLabelsColorMapSync());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to update dashboard color on load:', e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Ensure that the stored color map matches fresh map.
|
||||
*
|
||||
* @param {*} metadata - the dashboard metadata
|
||||
* @returns void
|
||||
*/
|
||||
export const ensureSyncedLabelsColorMap = metadata => (dispatch, getState) => {
|
||||
const {
|
||||
dashboardState: { labelsColorMapMustSync },
|
||||
} = getState();
|
||||
const updatedMetadata = { ...metadata };
|
||||
const customLabelsColor = metadata.label_colors || {};
|
||||
const isMapSynced = isLabelsColorMapSynced(metadata);
|
||||
const mustSync = !isMapSynced;
|
||||
|
||||
if (mustSync) {
|
||||
const freshestColorMapEntries = getLabelsColorMapEntries(customLabelsColor);
|
||||
updatedMetadata.map_label_colors = freshestColorMapEntries;
|
||||
dispatch(setDashboardMetadata(updatedMetadata));
|
||||
}
|
||||
|
||||
if (mustSync && !labelsColorMapMustSync) {
|
||||
// prepare to persist the just applied labels color map
|
||||
dispatch(setDashboardLabelsColorMapSync());
|
||||
}
|
||||
if (!mustSync && labelsColorMapMustSync) {
|
||||
dispatch(setDashboardLabelsColorMapSynced());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Ensure that the stored shared labels colors match current.
|
||||
*
|
||||
* @param {*} metadata - the dashboard metadata
|
||||
* @returns void
|
||||
*/
|
||||
export const ensureSyncedSharedLabelsColors =
|
||||
metadata => (dispatch, getState) => {
|
||||
// using a timeout to let the rendered charts finish processing labels
|
||||
setTimeout(() => {
|
||||
const {
|
||||
dashboardState: { sharedLabelsColorsMustSync },
|
||||
} = getState();
|
||||
const updatedMetadata = { ...metadata };
|
||||
const sharedLabelsColors = metadata.shared_label_colors || [];
|
||||
const freshLabelsColors = getFreshSharedLabels(sharedLabelsColors);
|
||||
const isSharedLabelsColorsSynced = isEqual(
|
||||
sharedLabelsColors,
|
||||
freshLabelsColors,
|
||||
);
|
||||
|
||||
const mustSync = !isSharedLabelsColorsSynced;
|
||||
|
||||
if (mustSync) {
|
||||
updatedMetadata.shared_label_colors = freshLabelsColors;
|
||||
dispatch(setDashboardMetadata(updatedMetadata));
|
||||
}
|
||||
|
||||
if (mustSync && !sharedLabelsColorsMustSync) {
|
||||
// prepare to persist the shared labels colors
|
||||
dispatch(setDashboardSharedLabelsColorsSync());
|
||||
}
|
||||
if (!mustSync && sharedLabelsColorsMustSync) {
|
||||
dispatch(setDashboardSharedLabelsColorsSynced());
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Updates the color map with new labels and colors as they appear.
|
||||
*
|
||||
* @param {*} renderedChartIds - the charts that have finished rendering
|
||||
* @returns void
|
||||
*/
|
||||
export const updateDashboardLabelsColor =
|
||||
renderedChartIds => (dispatch, getState) => {
|
||||
try {
|
||||
const {
|
||||
dashboardInfo: { metadata },
|
||||
charts,
|
||||
} = getState();
|
||||
const colorScheme = metadata.color_scheme;
|
||||
const labelsColorMapInstance = getLabelsColorMap();
|
||||
const fullLabelsColors = metadata.map_label_colors || {};
|
||||
const sharedLabelsColors = metadata.shared_label_colors || [];
|
||||
const customLabelsColors = metadata.label_colors || {};
|
||||
const updatedMetadata = { ...metadata };
|
||||
|
||||
// for dashboards with no color scheme, the charts should always use their individual schemes
|
||||
// this logic looks for unique labels (not shared across multiple charts) of each rendered chart
|
||||
// it applies a new color to those unique labels when the applied scheme is not up to date
|
||||
// while leaving shared label colors and custom label colors intact for color consistency
|
||||
const shouldReset = [];
|
||||
if (renderedChartIds.length > 0) {
|
||||
const sharedLabelsSet = new Set(sharedLabelsColors);
|
||||
renderedChartIds.forEach(id => {
|
||||
const chart = charts[id];
|
||||
const formData = chart.form_data || chart.latestQueryFormData;
|
||||
// ensure charts have their original color scheme always available
|
||||
labelsColorMapInstance.setOwnColorScheme(
|
||||
formData.slice_id,
|
||||
formData.color_scheme,
|
||||
);
|
||||
|
||||
// if dashboard has a scheme, charts should ignore individual schemes
|
||||
// thus following logic is inapplicable if a dashboard color scheme exists
|
||||
if (colorScheme) return;
|
||||
|
||||
const chartColorScheme = formData.color_scheme;
|
||||
const currentChartConfig = labelsColorMapInstance.chartsLabelsMap.get(
|
||||
formData.slice_id,
|
||||
);
|
||||
const currentChartLabels = currentChartConfig?.labels || [];
|
||||
const uniqueChartLabels = currentChartLabels.filter(
|
||||
l =>
|
||||
!sharedLabelsSet.has(l) && !customLabelsColors.hasOwnProperty(l),
|
||||
);
|
||||
|
||||
// Map unique labels to colors
|
||||
const uniqueChartLabelsColor = new Set(
|
||||
uniqueChartLabels.map(l => fullLabelsColors[l]).filter(Boolean),
|
||||
);
|
||||
|
||||
const expectedColorsForChartScheme = new Set(
|
||||
getColorSchemeDomain(chartColorScheme),
|
||||
);
|
||||
|
||||
// Check if any unique label color is not in the expected colors set
|
||||
const shouldResetColors = [...uniqueChartLabelsColor].some(
|
||||
color => !expectedColorsForChartScheme.has(color),
|
||||
);
|
||||
|
||||
// Only push uniqueChartLabels if they require resetting
|
||||
if (shouldResetColors) shouldReset.push(...uniqueChartLabels);
|
||||
});
|
||||
}
|
||||
|
||||
// an existing map is available, use mrge option
|
||||
// to only apply colors to newly found labels
|
||||
const shouldGoFresh = shouldReset.length > 0 ? shouldReset : false;
|
||||
const shouldMerge = !shouldGoFresh;
|
||||
// re-apply the color map first to get fresh maps accordingly
|
||||
applyColors(updatedMetadata, shouldGoFresh, shouldMerge);
|
||||
// new data may have appeared in the map (data changes)
|
||||
// or new slices may have appeared while changing tabs
|
||||
dispatch(ensureSyncedLabelsColorMap(updatedMetadata));
|
||||
dispatch(ensureSyncedSharedLabelsColors(updatedMetadata));
|
||||
} catch (e) {
|
||||
console.error('Failed to update colors for new charts and labels:', e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -157,9 +157,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh when native filters changed', () => {
|
||||
getRelatedCharts.mockReturnValue({
|
||||
[NATIVE_FILTER_ID]: [230],
|
||||
});
|
||||
getRelatedCharts.mockReturnValue([230]);
|
||||
wrapper.setProps({
|
||||
activeFilters: {
|
||||
...OVERRIDE_FILTERS,
|
||||
@@ -191,13 +189,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh if a filter is added', () => {
|
||||
getRelatedCharts.mockReturnValue({
|
||||
'1_region': [1],
|
||||
'2_country_name': [1, 2],
|
||||
'3_region': [1],
|
||||
'3_country_name': [],
|
||||
gender: [1],
|
||||
});
|
||||
getRelatedCharts.mockReturnValue([1]);
|
||||
const newFilter = {
|
||||
gender: { values: ['boy', 'girl'], scope: [1] },
|
||||
};
|
||||
@@ -209,12 +201,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh if a filter is removed', () => {
|
||||
getRelatedCharts.mockReturnValue({
|
||||
'1_region': [1],
|
||||
'2_country_name': [1, 2],
|
||||
'3_region': [1],
|
||||
'3_country_name': [],
|
||||
});
|
||||
getRelatedCharts.mockReturnValue([]);
|
||||
wrapper.setProps({
|
||||
activeFilters: {},
|
||||
});
|
||||
@@ -223,6 +210,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh if a filter is changed', () => {
|
||||
getRelatedCharts.mockReturnValue([1]);
|
||||
const newFilters = {
|
||||
...OVERRIDE_FILTERS,
|
||||
'1_region': { values: ['Canada'], scope: [1] },
|
||||
@@ -236,6 +224,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh with multiple chart ids', () => {
|
||||
getRelatedCharts.mockReturnValue([1, 2]);
|
||||
const newFilters = {
|
||||
...OVERRIDE_FILTERS,
|
||||
'2_country_name': { values: ['New Country'], scope: [1, 2] },
|
||||
@@ -262,6 +251,7 @@ describe('Dashboard', () => {
|
||||
});
|
||||
|
||||
it('should call refresh with empty [] if a filter is changed but scope is not applicable', () => {
|
||||
getRelatedCharts.mockReturnValue([]);
|
||||
const newFilters = {
|
||||
...OVERRIDE_FILTERS,
|
||||
'3_country_name': { values: ['CHINA'], scope: [] },
|
||||
|
||||
@@ -36,6 +36,8 @@ import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
|
||||
|
||||
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
|
||||
|
||||
fetchMock.put('glob:*/api/v1/dashboard/*', {});
|
||||
|
||||
jest.mock('src/dashboard/actions/dashboardState', () => ({
|
||||
...jest.requireActual('src/dashboard/actions/dashboardState'),
|
||||
fetchFaveStar: jest.fn(),
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
// ParentSize uses resize observer so the dashboard will update size
|
||||
// when its container size changes, due to e.g., builder side panel opening
|
||||
import { FC, useEffect, useMemo, useRef } from 'react';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
Filter,
|
||||
@@ -43,12 +43,12 @@ import {
|
||||
import { getChartIdsInFilterScope } from 'src/dashboard/util/getChartIdsInFilterScope';
|
||||
import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId';
|
||||
import { setInScopeStatusOfFilters } from 'src/dashboard/actions/nativeFilters';
|
||||
import { updateDashboardLabelsColor } from 'src/dashboard/actions/dashboardState';
|
||||
import {
|
||||
applyColors,
|
||||
getColorNamespace,
|
||||
resetColors,
|
||||
} from 'src/utils/colorScheme';
|
||||
applyDashboardLabelsColorOnLoad,
|
||||
updateDashboardLabelsColor,
|
||||
persistDashboardLabelsColor,
|
||||
} from 'src/dashboard/actions/dashboardState';
|
||||
import { getColorNamespace, resetColors } from 'src/utils/colorScheme';
|
||||
import { NATIVE_FILTER_DIVIDER_PREFIX } from '../nativeFilters/FiltersConfigModal/utils';
|
||||
import { findTabsWithChartsInScope } from '../nativeFilters/utils';
|
||||
import { getRootLevelTabsComponent } from './utils';
|
||||
@@ -88,6 +88,14 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
const chartIds = useSelector<RootState, number[]>(state =>
|
||||
Object.values(state.charts).map(chart => chart.id),
|
||||
);
|
||||
const renderedChartIds = useSelector<RootState, number[]>(state =>
|
||||
Object.values(state.charts)
|
||||
.filter(chart => chart.chartStatus === 'rendered')
|
||||
.map(chart => chart.id),
|
||||
);
|
||||
const [dashboardLabelsColorInitiated, setDashboardLabelsColorInitiated] =
|
||||
useState(false);
|
||||
const prevRenderedChartIds = useRef<number[]>([]);
|
||||
|
||||
const prevTabIndexRef = useRef();
|
||||
const tabIndex = useMemo(() => {
|
||||
@@ -140,28 +148,65 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
const activeKey = min === 0 ? DASHBOARD_GRID_ID : min.toString();
|
||||
const TOP_OF_PAGE_RANGE = 220;
|
||||
|
||||
const onBeforeUnload = useCallback(() => {
|
||||
dispatch(persistDashboardLabelsColor());
|
||||
resetColors(getColorNamespace(dashboardInfo?.metadata?.color_namespace));
|
||||
prevRenderedChartIds.current = [];
|
||||
}, [dashboardInfo?.metadata?.color_namespace, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// verify freshness of color map on tab change
|
||||
// and when loading for first time
|
||||
setTimeout(() => {
|
||||
dispatch(updateDashboardLabelsColor());
|
||||
}, 500);
|
||||
}, [directPathToChild, dispatch]);
|
||||
// verify freshness of color map
|
||||
// when charts render to catch new labels
|
||||
const numRenderedCharts = renderedChartIds.length;
|
||||
|
||||
if (
|
||||
dashboardLabelsColorInitiated &&
|
||||
dashboardInfo?.id &&
|
||||
numRenderedCharts > 0 &&
|
||||
prevRenderedChartIds.current.length < numRenderedCharts
|
||||
) {
|
||||
const newRenderedChartIds = renderedChartIds.filter(
|
||||
id => !prevRenderedChartIds.current.includes(id),
|
||||
);
|
||||
prevRenderedChartIds.current = renderedChartIds;
|
||||
dispatch(updateDashboardLabelsColor(newRenderedChartIds));
|
||||
}
|
||||
}, [
|
||||
dashboardInfo?.id,
|
||||
renderedChartIds,
|
||||
dispatch,
|
||||
dashboardLabelsColorInitiated,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
const colorNamespace = getColorNamespace(
|
||||
dashboardInfo?.metadata?.color_namespace,
|
||||
);
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
// apply labels color as dictated by stored metadata
|
||||
applyColors(dashboardInfo.metadata);
|
||||
|
||||
if (dashboardInfo?.id && !dashboardLabelsColorInitiated) {
|
||||
// apply labels color as dictated by stored metadata (if any)
|
||||
setDashboardLabelsColorInitiated(true);
|
||||
dispatch(applyDashboardLabelsColorOnLoad(dashboardInfo.metadata));
|
||||
}
|
||||
|
||||
return () => {
|
||||
resetColors(getColorNamespace(colorNamespace));
|
||||
onBeforeUnload();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardInfo.id, dispatch]);
|
||||
}, [dashboardInfo?.id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// 'beforeunload' event interferes with Cypress data cleanup process.
|
||||
// This code prevents 'beforeunload' from triggering in Cypress tests,
|
||||
// as it is not required for end-to-end testing scenarios.
|
||||
if (!(window as any).Cypress) {
|
||||
window.addEventListener('beforeunload', onBeforeUnload);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||
};
|
||||
}, [onBeforeUnload]);
|
||||
|
||||
return (
|
||||
<div className="grid-container" data-test="grid-container">
|
||||
|
||||
@@ -373,3 +373,48 @@ test('should render an extension component if one is supplied', () => {
|
||||
screen.getByText('dashboard.nav.right extension component'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT render MetadataBar when in edit mode', () => {
|
||||
const mockedProps = {
|
||||
...createProps(),
|
||||
editMode: true,
|
||||
dashboardInfo: {
|
||||
...createProps().dashboardInfo,
|
||||
userId: '123',
|
||||
},
|
||||
};
|
||||
setup(mockedProps);
|
||||
expect(
|
||||
screen.queryByText(mockedProps.dashboardInfo.changed_on_delta_humanized),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT render MetadataBar when embedded', () => {
|
||||
const mockedProps = {
|
||||
...createProps(),
|
||||
editMode: false,
|
||||
dashboardInfo: {
|
||||
...createProps().dashboardInfo,
|
||||
userId: undefined,
|
||||
},
|
||||
};
|
||||
setup(mockedProps);
|
||||
expect(
|
||||
screen.queryByText(mockedProps.dashboardInfo.changed_on_delta_humanized),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render MetadataBar when not in edit mode and not embedded', () => {
|
||||
const mockedProps = {
|
||||
...createProps(),
|
||||
editMode: false,
|
||||
dashboardInfo: {
|
||||
...createProps().dashboardInfo,
|
||||
userId: '123',
|
||||
},
|
||||
};
|
||||
setup(mockedProps);
|
||||
expect(
|
||||
screen.getByText(mockedProps.dashboardInfo.changed_on_delta_humanized),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -461,7 +461,6 @@ class Header extends PureComponent {
|
||||
customCss,
|
||||
colorNamespace,
|
||||
dataMask,
|
||||
setColorScheme,
|
||||
setUnsavedChanges,
|
||||
colorScheme,
|
||||
onUndo,
|
||||
@@ -496,11 +495,11 @@ class Header extends PureComponent {
|
||||
const refreshWarning =
|
||||
dashboardInfo.common?.conf
|
||||
?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE;
|
||||
const isEmbedded = !dashboardInfo?.userId;
|
||||
|
||||
const handleOnPropertiesChange = updates => {
|
||||
const { dashboardInfoChanged, dashboardTitleChanged } = this.props;
|
||||
|
||||
setColorScheme(updates.colorScheme);
|
||||
dashboardInfoChanged({
|
||||
slug: updates.slug,
|
||||
metadata: JSON.parse(updates.jsonMetadata || '{}'),
|
||||
@@ -553,7 +552,7 @@ class Header extends PureComponent {
|
||||
visible={!editMode}
|
||||
/>
|
||||
),
|
||||
!editMode && (
|
||||
!editMode && !isEmbedded && (
|
||||
<MetadataBar
|
||||
items={this.getMetadataItems()}
|
||||
tooltipPlacement="bottom"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import { Input } from 'src/components/Input';
|
||||
import { FormItem } from 'src/components/Form';
|
||||
@@ -44,9 +44,19 @@ import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import TagType from 'src/types/TagType';
|
||||
import { fetchTags, OBJECT_TYPES } from 'src/features/tags/tags';
|
||||
import { loadTags } from 'src/components/Tags/utils';
|
||||
import { applyColors, getColorNamespace } from 'src/utils/colorScheme';
|
||||
import {
|
||||
applyColors,
|
||||
getColorNamespace,
|
||||
getLabelsColorMapEntries,
|
||||
} from 'src/utils/colorScheme';
|
||||
import getOwnerName from 'src/utils/getOwnerName';
|
||||
import Owner from 'src/types/Owner';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
setColorScheme,
|
||||
setDashboardMetadata,
|
||||
} from 'src/dashboard/actions/dashboardState';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
|
||||
const StyledFormItem = styled(FormItem)`
|
||||
margin-bottom: 0;
|
||||
@@ -84,6 +94,7 @@ type DashboardInfo = {
|
||||
certifiedBy: string;
|
||||
certificationDetails: string;
|
||||
isManagedExternally: boolean;
|
||||
metadata: Record<string, any>;
|
||||
};
|
||||
|
||||
const PropertiesModal = ({
|
||||
@@ -98,10 +109,11 @@ const PropertiesModal = ({
|
||||
onSubmit = () => {},
|
||||
show = false,
|
||||
}: PropertiesModalProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const [form] = AntdForm.useForm();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
|
||||
const [colorScheme, setColorScheme] = useState(currentColorScheme);
|
||||
const [colorScheme, setCurrentColorScheme] = useState(currentColorScheme);
|
||||
const [jsonMetadata, setJsonMetadata] = useState('');
|
||||
const [dashboardInfo, setDashboardInfo] = useState<DashboardInfo>();
|
||||
const [owners, setOwners] = useState<Owners>([]);
|
||||
@@ -109,6 +121,7 @@ const PropertiesModal = ({
|
||||
const saveLabel = onlyApply ? t('Apply') : t('Save');
|
||||
const [tags, setTags] = useState<TagType[]>([]);
|
||||
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
|
||||
const originalDashboardMetadata = useRef<Record<string, any>>({});
|
||||
|
||||
const tagsAsSelectValues = useMemo(() => {
|
||||
const selectTags = tags.map((tag: { id: number; name: string }) => ({
|
||||
@@ -182,21 +195,24 @@ const PropertiesModal = ({
|
||||
certifiedBy: certified_by || '',
|
||||
certificationDetails: certification_details || '',
|
||||
isManagedExternally: is_managed_externally || false,
|
||||
metadata,
|
||||
};
|
||||
|
||||
form.setFieldsValue(dashboardInfo);
|
||||
setDashboardInfo(dashboardInfo);
|
||||
setOwners(owners);
|
||||
setRoles(roles);
|
||||
setColorScheme(metadata.color_scheme);
|
||||
setCurrentColorScheme(metadata.color_scheme);
|
||||
|
||||
const metaDataCopy = omit(metadata, [
|
||||
'positions',
|
||||
'shared_label_colors',
|
||||
'map_label_colors',
|
||||
'color_scheme_domain',
|
||||
]);
|
||||
|
||||
setJsonMetadata(metaDataCopy ? jsonStringify(metaDataCopy) : '');
|
||||
originalDashboardMetadata.current = metadata;
|
||||
},
|
||||
[form],
|
||||
);
|
||||
@@ -269,6 +285,8 @@ const PropertiesModal = ({
|
||||
return parsedRoles;
|
||||
};
|
||||
|
||||
const handleOnCancel = () => onHide();
|
||||
|
||||
const onColorSchemeChange = (
|
||||
colorScheme = '',
|
||||
{ updateMetadata = true } = {},
|
||||
@@ -287,20 +305,21 @@ const PropertiesModal = ({
|
||||
throw new Error('A valid color scheme is required');
|
||||
}
|
||||
|
||||
jsonMetadataObj.color_scheme = colorScheme;
|
||||
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
|
||||
|
||||
setCurrentColorScheme(colorScheme);
|
||||
dispatch(setColorScheme(colorScheme));
|
||||
|
||||
// update metadata to match selection
|
||||
if (updateMetadata) {
|
||||
jsonMetadataObj.color_scheme = colorScheme;
|
||||
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
|
||||
|
||||
setJsonMetadata(jsonStringify(jsonMetadataObj));
|
||||
}
|
||||
setColorScheme(colorScheme);
|
||||
};
|
||||
|
||||
const onFinish = () => {
|
||||
const { title, slug, certifiedBy, certificationDetails } =
|
||||
form.getFieldsValue();
|
||||
let currentColorScheme = colorScheme;
|
||||
let currentJsonMetadata = jsonMetadata;
|
||||
|
||||
// validate currentJsonMetadata
|
||||
@@ -318,29 +337,48 @@ const PropertiesModal = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const copyMetadata = { ...metadata };
|
||||
const colorNamespace = getColorNamespace(metadata?.color_namespace);
|
||||
|
||||
// color scheme in json metadata has precedence over selection
|
||||
currentColorScheme = metadata?.color_scheme || colorScheme;
|
||||
const updatedColorScheme = metadata?.color_scheme || colorScheme;
|
||||
const shouldGoFresh =
|
||||
updatedColorScheme !== originalDashboardMetadata.current.color_scheme;
|
||||
const shouldResetCustomLabels = !areObjectsEqual(
|
||||
originalDashboardMetadata.current.label_colors || {},
|
||||
metadata?.label_colors || {},
|
||||
);
|
||||
const currentCustomLabels = Object.keys(metadata?.label_colors || {});
|
||||
const prevCustomLabels = Object.keys(
|
||||
originalDashboardMetadata.current.label_colors || {},
|
||||
);
|
||||
const resettableCustomLabels =
|
||||
currentCustomLabels.length > 0 ? currentCustomLabels : prevCustomLabels;
|
||||
const freshCustomLabels =
|
||||
shouldResetCustomLabels && resettableCustomLabels.length > 0
|
||||
? resettableCustomLabels
|
||||
: false;
|
||||
const jsonMetadataObj = getJsonMetadata();
|
||||
const customLabelColors = jsonMetadataObj.label_colors || {};
|
||||
const updatedDashboardMetadata = {
|
||||
...originalDashboardMetadata.current,
|
||||
label_colors: customLabelColors,
|
||||
color_scheme: updatedColorScheme,
|
||||
};
|
||||
|
||||
// remove information from user facing input
|
||||
if (metadata?.shared_label_colors) {
|
||||
delete metadata.shared_label_colors;
|
||||
}
|
||||
if (metadata?.color_scheme_domain) {
|
||||
delete metadata.color_scheme_domain;
|
||||
}
|
||||
originalDashboardMetadata.current = updatedDashboardMetadata;
|
||||
applyColors(updatedDashboardMetadata, shouldGoFresh || freshCustomLabels);
|
||||
dispatch(
|
||||
setDashboardMetadata({
|
||||
...updatedDashboardMetadata,
|
||||
map_label_colors: getLabelsColorMapEntries(customLabelColors),
|
||||
}),
|
||||
);
|
||||
|
||||
// only apply colors, the user has not saved yet
|
||||
applyColors(copyMetadata, true);
|
||||
|
||||
currentJsonMetadata = jsonStringify(metadata);
|
||||
|
||||
onColorSchemeChange(currentColorScheme, {
|
||||
onColorSchemeChange(updatedColorScheme, {
|
||||
updateMetadata: false,
|
||||
});
|
||||
|
||||
currentJsonMetadata = jsonStringify(metadata);
|
||||
|
||||
const moreOnSubmitProps: { roles?: Roles } = {};
|
||||
const morePutProps: { roles?: number[]; tags?: (number | undefined)[] } =
|
||||
{};
|
||||
@@ -557,14 +595,14 @@ const PropertiesModal = ({
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
onHide={handleOnCancel}
|
||||
title={t('Dashboard properties')}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
htmlType="button"
|
||||
buttonSize="small"
|
||||
onClick={onHide}
|
||||
onClick={handleOnCancel}
|
||||
data-test="properties-modal-cancel-button"
|
||||
cta
|
||||
>
|
||||
|
||||
@@ -66,8 +66,8 @@ const SyncDashboardState: FC<Props> = ({ dashboardPageId }) => {
|
||||
>(
|
||||
({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
|
||||
labelsColor: dashboardInfo.metadata?.label_colors || EMPTY_OBJECT,
|
||||
labelsColorMap:
|
||||
dashboardInfo.metadata?.shared_label_colors || EMPTY_OBJECT,
|
||||
labelsColorMap: dashboardInfo.metadata?.map_label_colors || EMPTY_OBJECT,
|
||||
sharedLabelsColors: dashboardInfo.metadata?.shared_label_colors || [],
|
||||
colorScheme: dashboardState?.colorScheme,
|
||||
chartConfiguration:
|
||||
dashboardInfo.metadata?.chart_configuration || EMPTY_OBJECT,
|
||||
|
||||
@@ -197,11 +197,19 @@ class Chart extends Component {
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
// chart should re-render if color scheme or label color was changed
|
||||
// chart should re-render if color scheme or label colors were changed
|
||||
nextProps.formData?.color_scheme !== this.props.formData?.color_scheme ||
|
||||
!areObjectsEqual(
|
||||
nextProps.formData?.label_colors,
|
||||
this.props.formData?.label_colors,
|
||||
nextProps.formData?.label_colors || {},
|
||||
this.props.formData?.label_colors || {},
|
||||
) ||
|
||||
!areObjectsEqual(
|
||||
nextProps.formData?.map_label_colors || {},
|
||||
this.props.formData?.map_label_colors || {},
|
||||
) ||
|
||||
!isEqual(
|
||||
nextProps.formData?.shared_label_colors || [],
|
||||
this.props.formData?.shared_label_colors || [],
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { ResizeCallback, ResizeStartCallback } from 're-resizable';
|
||||
import cx from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { css } from '@superset-ui/core';
|
||||
import { css, useTheme } from '@superset-ui/core';
|
||||
import { LayoutItem, RootState } from 'src/dashboard/types';
|
||||
import AnchorLink from 'src/dashboard/components/AnchorLink';
|
||||
import Chart from 'src/dashboard/containers/Chart';
|
||||
@@ -70,15 +70,6 @@ interface ChartHolderProps {
|
||||
isInView: boolean;
|
||||
}
|
||||
|
||||
const fullSizeStyle = css`
|
||||
&& {
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChartHolder: React.FC<ChartHolderProps> = ({
|
||||
id,
|
||||
parentId,
|
||||
@@ -102,6 +93,16 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
|
||||
setFullSizeChartId,
|
||||
isInView,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const fullSizeStyle = css`
|
||||
&& {
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
`;
|
||||
const { chartId } = component.meta;
|
||||
const isFullSize = fullSizeChartId === chartId;
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function DownloadScreenshot({
|
||||
anchor,
|
||||
activeTabs,
|
||||
dataMask,
|
||||
urlParams: getDashboardUrlParams(),
|
||||
urlParams: getDashboardUrlParams(['edit']),
|
||||
},
|
||||
})
|
||||
.then(({ json }) => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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.',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,6 +61,9 @@ describe('controlUtils', () => {
|
||||
controls: {},
|
||||
form_data: { datasource: '1__table', viz_type: 'table' },
|
||||
common: {},
|
||||
slice: {
|
||||
slice_id: 1,
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user