Compare commits

..

16 Commits

Author SHA1 Message Date
Maxime Beauchemin
e5369ab0f0 chore: ran 'npm audit fix' 2024-11-22 17:01:50 -08:00
Maxime Beauchemin
e0deb704f9 feat: make ephemeral env use supersetbot + deprecate build_docker.py (#30870) 2024-11-22 14:19:08 -08:00
Kamil Gabryjelski
abf3790ea6 chore: Cleanup code related to MetadataBar, fix types (#31030) 2024-11-22 16:02:13 +01:00
Kamil Gabryjelski
f8adaf66c1 chore: Refactor dashboard header to func component (#31029) 2024-11-22 14:10:46 +01:00
Beto Dealmeida
832fed1db5 fix(dataset): use sqlglot for DML check (#31024) 2024-11-22 07:21:05 -05:00
github-actions[bot]
ccce9abf57 chore(🦾): bump python cattrs 23.2.3 -> 24.1.2 (#30998)
Co-authored-by: GitHub Action <action@github.com>
2024-11-21 15:15:15 -08:00
Seiya
7d60a88048 docs: Update doc about CSV upload (#30867)
Co-authored-by: Evan Rusackas <evan@preset.io>
2024-11-21 16:15:08 -07:00
Linden
8905508d8f fix(imports): import query_context for imports with charts (#30887) 2024-11-21 16:14:45 -07:00
Giampaolo Capelli
0b647b2dcc docs: Embedded sdk (#30972) 2024-11-21 16:14:18 -07:00
David Hotham
f58b5295a9 chore: publish wheels (#30981) 2024-11-21 12:18:27 -08:00
JUST.in DO IT
9e5b568cc9 fix(explore): verified props is not updated (#31008) 2024-11-21 11:05:43 -08:00
Geido
90572be95a fix(Dashboard): Retain colors when color scheme not set (#30646) 2024-11-21 19:58:32 +02:00
github-actions[bot]
3c326598a8 chore(🦾): bump python flask-babel subpackage(s) (#31000)
Co-authored-by: GitHub Action <action@github.com>
2024-11-21 09:06:58 -08:00
github-actions[bot]
ae04959175 chore(🦾): bump python cffi 1.16.0 -> 1.17.1 (#31002)
Co-authored-by: GitHub Action <action@github.com>
2024-11-21 09:05:28 -08:00
github-actions[bot]
1e224398e1 chore(🦾): bump python numexpr 2.10.0 -> 2.10.1 (#31006)
Co-authored-by: GitHub Action <action@github.com>
2024-11-21 09:04:43 -08:00
Beto Dealmeida
fa7b66424c chore: add unit tests for is_mutating() (#31021) 2024-11-21 11:08:53 -05:00
113 changed files with 3549 additions and 2505 deletions

View File

@@ -1,30 +1,25 @@
name: Ephemeral env workflow
# Example manual trigger: gh workflow run ephemeral-env.yml --ref fix_ephemerals --field comment_body="/testenv up" --field issue_number=666
on:
issue_comment:
types: [created]
workflow_dispatch:
inputs:
comment_body:
description: 'Comment body to simulate /testenv command'
required: true
default: '/testenv up'
issue_number:
description: 'Issue or PR number'
required: true
jobs:
config:
runs-on: "ubuntu-22.04"
if: github.event.issue.pull_request
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
- name: "Check for secrets"
id: check
shell: bash
run: |
if [ -n "${{ (secrets.AWS_ACCESS_KEY_ID != '' && secrets.AWS_SECRET_ACCESS_KEY != '') || '' }}" ]; then
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
fi
ephemeral-env-comment:
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}-comment
group: ${{ github.workflow }}-${{ github.event.inputs.issue_number || github.event.issue.number || github.run_id }}-comment
cancel-in-progress: true
needs: config
if: needs.config.outputs.has-secrets
name: Evaluate ephemeral env comment trigger (/testenv)
runs-on: ubuntu-22.04
permissions:
@@ -44,18 +39,18 @@ jobs:
with:
result-encoding: string
script: |
const pattern = /^\/testenv (up|down)/
const result = pattern.exec(context.payload.comment.body)
return result === null ? 'noop' : result[1]
const pattern = /^\/testenv (up|down)/;
const result = pattern.exec('${{ github.event.inputs.comment_body || github.event.comment.body }}');
return result === null ? 'noop' : result[1];
- name: Eval comment body for feature flags
- name: Looking for feature flags
uses: actions/github-script@v7
id: eval-feature-flags
with:
script: |
const pattern = /FEATURE_(\w+)=(\w+)/g;
let results = [];
[...context.payload.comment.body.matchAll(pattern)].forEach(match => {
[...'${{ github.event.inputs.comment_body || github.event.comment.body }}'.matchAll(pattern)].forEach(match => {
const config = {
name: `SUPERSET_FEATURE_${match[1]}`,
value: match[2],
@@ -67,24 +62,25 @@ jobs:
- name: Limit to committers
if: >
steps.eval-body.outputs.result != 'noop' &&
github.event_name == 'issue_comment' &&
github.event.comment.author_association != 'MEMBER' &&
github.event.comment.author_association != 'OWNER'
uses: actions/github-script@v7
with:
github-token: ${{github.token}}
github-token: ${{ github.token }}
script: |
const errMsg = '@${{ github.event.comment.user.login }} Ephemeral environment creation is currently limited to committers.'
const errMsg = '@${{ github.event.comment.user.login }} Ephemeral environment creation is currently limited to committers.';
github.rest.issues.createComment({
issue_number: ${{ github.event.issue.number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: errMsg
})
core.setFailed(errMsg)
});
core.setFailed(errMsg);
ephemeral-docker-build:
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}-build
group: ${{ github.workflow }}-${{ github.event.inputs.issue_number || github.event.issue.number || github.run_id }}-build
cancel-in-progress: true
needs: ephemeral-env-comment
name: ephemeral-docker-build
@@ -98,9 +94,9 @@ jobs:
const request = {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.issue.number }},
}
core.info(`Getting PR #${request.pull_number} from ${request.owner}/${request.repo}`)
pull_number: ${{ github.event.inputs.issue_number || github.event.issue.number }},
};
core.info(`Getting PR #${request.pull_number} from ${request.owner}/${request.repo}`);
const pr = await github.rest.pulls.get(request);
return pr.data;
@@ -121,12 +117,17 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Setup supersetbot
uses: ./.github/actions/setup-supersetbot/
- name: Build ephemeral env image
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
./scripts/build_docker.py \
"ci" \
"pull_request" \
--build_context_ref ${{ github.event.issue.number }}
supersetbot docker \
--preset ci \
--platform linux/amd64 \
--context-ref "$RELEASE"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
@@ -146,7 +147,7 @@ jobs:
ECR_REPOSITORY: superset-ci
IMAGE_TAG: apache/superset:${{ steps.get-sha.outputs.sha }}-ci
run: |
docker tag $IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:pr-${{ github.event.issue.number }}-ci
docker tag $IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-ci
docker push -a $ECR_REGISTRY/$ECR_REPOSITORY
ephemeral-env-up:
@@ -181,22 +182,22 @@ jobs:
aws ecr describe-images \
--registry-id $(echo "${{ steps.login-ecr.outputs.registry }}" | grep -Eo "^[0-9]+") \
--repository-name superset-ci \
--image-ids imageTag=pr-${{ github.event.issue.number }}-ci
--image-ids imageTag=pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-ci
- name: Fail on missing container image
if: steps.check-image.outcome == 'failure'
uses: actions/github-script@v7
with:
github-token: ${{github.token}}
github-token: ${{ github.token }}
script: |
const errMsg = '@${{ github.event.comment.user.login }} Container image not yet published for this PR. Please try again when build is complete.'
const errMsg = '@${{ github.event.comment.user.login }} Container image not yet published for this PR. Please try again when build is complete.';
github.rest.issues.createComment({
issue_number: ${{ github.event.issue.number }},
issue_number: ${{ github.event.inputs.issue_number || github.event.issue.number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: errMsg
})
core.setFailed(errMsg)
});
core.setFailed(errMsg);
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
@@ -204,7 +205,7 @@ jobs:
with:
task-definition: .github/workflows/ecs-task-definition.json
container-name: superset-ci
image: ${{ steps.login-ecr.outputs.registry }}/superset-ci:pr-${{ github.event.issue.number }}-ci
image: ${{ steps.login-ecr.outputs.registry }}/superset-ci:pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-ci
- name: Update env vars in the Amazon ECS task definition
run: |
@@ -213,13 +214,10 @@ jobs:
- name: Describe ECS service
id: describe-services
run: |
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${{ github.event.issue.number }}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
- name: Create ECS service
if: steps.describe-services.outputs.active != 'true'
id: create-service
env:
ECR_SUBNETS: subnet-0e15a5034b4121710,subnet-0e8efef4a72224974
ECR_SECURITY_GROUP: sg-092ff3a6ae0574d91
if: steps.describe-services.outputs.active != 'true'
run: |
aws ecs create-service \
--cluster superset-ci \
@@ -230,7 +228,6 @@ jobs:
--platform-version LATEST \
--network-configuration "awsvpcConfiguration={subnets=[$ECR_SUBNETS],securityGroups=[$ECR_SECURITY_GROUP],assignPublicIp=ENABLED}" \
--tags key=pr,value=${{ github.event.issue.number }} key=github_user,value=${{ github.actor }}
- name: Deploy Amazon ECS task definition
id: deploy-task
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
@@ -245,17 +242,14 @@ jobs:
id: list-tasks
run: |
echo "task=$(aws ecs list-tasks --cluster superset-ci --service-name pr-${{ github.event.issue.number }}-service | jq '.taskArns | first')" >> $GITHUB_OUTPUT
- name: Get network interface
id: get-eni
run: |
echo "eni=$(aws ecs describe-tasks --cluster superset-ci --tasks ${{ steps.list-tasks.outputs.task }} | jq '.tasks | .[0] | .attachments | .[0] | .details | map(select(.name=="networkInterfaceId")) | .[0] | .value')" >> $GITHUB_OUTPUT
echo "eni=$(aws ecs describe-tasks --cluster superset-ci --tasks ${{ steps.list-tasks.outputs.task }} | jq '.tasks | .[0] | .attachments | .[0] | .details | map(select(.name==\"networkInterfaceId\")) | .[0] | .value')" >> $GITHUB_OUTPUT
- name: Get public IP
id: get-ip
run: |
echo "ip=$(aws ec2 describe-network-interfaces --network-interface-ids ${{ steps.get-eni.outputs.eni }} | jq -r '.NetworkInterfaces | first | .Association.PublicIp')" >> $GITHUB_OUTPUT
- name: Comment (success)
if: ${{ success() }}
uses: actions/github-script@v7
@@ -263,12 +257,11 @@ jobs:
github-token: ${{github.token}}
script: |
github.rest.issues.createComment({
issue_number: ${{ github.event.issue.number }},
issue_number: ${{ github.event.inputs.issue_number || github.event.issue.number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: '@${{ github.event.comment.user.login }} Ephemeral environment spinning up at http://${{ steps.get-ip.outputs.ip }}:8080. Credentials are `admin`/`admin`. Please allow several minutes for bootstrapping and startup.'
body: '@${{ github.event.inputs.user_login || github.event.comment.user.login }} Ephemeral environment spinning up at http://${{ steps.get-ip.outputs.ip }}:8080. Credentials are `admin`/`admin`. Please allow several minutes for bootstrapping and startup.'
})
- name: Comment (failure)
if: ${{ failure() }}
uses: actions/github-script@v7
@@ -276,8 +269,8 @@ jobs:
github-token: ${{github.token}}
script: |
github.rest.issues.createComment({
issue_number: ${{ github.event.issue.number }},
issue_number: ${{ github.event.inputs.issue_number || github.event.issue.number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: '@${{ github.event.comment.user.login }} Ephemeral environment creation failed. Please check the Actions logs for details.'
body: '@${{ github.event.inputs.user_login || github.event.comment.user.login }} Ephemeral environment creation failed. Please check the Actions logs for details.'
})

View File

@@ -115,7 +115,7 @@ RUN mkdir -p ${PYTHONPATH} superset/static requirements superset-frontend apache
libldap2-dev \
&& touch superset/static/version_info.json \
&& chown -R superset:superset ./* \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
COPY --chown=superset:superset pyproject.toml setup.py MANIFEST.in README.md ./
# setup.py uses the version information in package.json
@@ -128,7 +128,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
&& pip install --no-cache-dir --upgrade setuptools pip \
&& pip install --no-cache-dir -r requirements/base.txt \
&& apt-get autoremove -yqq --purge build-essential \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
# Copy the compiled frontend assets
COPY --chown=superset:superset --from=superset-node /app/superset/static/assets superset/static/assets
@@ -177,7 +177,7 @@ RUN apt-get update -qq \
libxtst6 \
git \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir playwright
@@ -199,13 +199,13 @@ RUN if [ "$INCLUDE_FIREFOX" = "true" ]; then \
&& wget -q https://github.com/mozilla/geckodriver/releases/download/${GECKODRIVER_VERSION}/geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz -O - | tar xfz - -C /usr/local/bin \
&& wget -q https://download-installer.cdn.mozilla.net/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/firefox-${FIREFOX_VERSION}.tar.bz2 -O - | tar xfj - -C /opt \
&& ln -s /opt/firefox/firefox /usr/local/bin/firefox \
&& apt-get autoremove -yqq --purge wget bzip2 && rm -rf /var/[log,tmp]/* /tmp/* /var/lib/apt/lists/*; \
&& apt-get autoremove -yqq --purge wget bzip2 && rm -rf /var/[log,tmp]/* /tmp/* /var/lib/apt/lists/* /var/cache/apt/archives/*; \
fi
# Installing mysql client os-level dependencies in dev image only because GPL
RUN apt-get install -yqq --no-install-recommends \
default-libmysqlclient-dev \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
COPY --chown=superset:superset requirements/development.txt requirements/
RUN --mount=type=cache,target=/root/.cache/pip \
@@ -213,7 +213,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
build-essential \
&& pip install --no-cache-dir -r requirements/development.txt \
&& apt-get autoremove -yqq --purge build-essential \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
USER superset
######################################################################

View File

@@ -437,7 +437,7 @@ cd ${SUPERSET_RELEASE_RC}
python3 -m venv venv
source venv/bin/activate
pip install -r requirements/base.txt
pip install twine
pip install build twine
```
Create the distribution
@@ -455,7 +455,7 @@ cd ../
./scripts/translations/generate_po_files.sh
# build the python distribution
python setup.py sdist
python -m build
```
Publish to PyPI
@@ -466,6 +466,7 @@ an account first if you don't have one, and reference your username
while requesting access to push packages.
```bash
twine upload dist/apache_superset-${SUPERSET_VERSION}-py3-none-any.whl
twine upload dist/apache-superset-${SUPERSET_VERSION}.tar.gz
```

View File

@@ -25,7 +25,6 @@ x-superset-user: &superset-user root
x-superset-depends-on: &superset-depends-on
- db
- redis
- superset-checks
x-superset-volumes: &superset-volumes
# /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
- ./docker:/app/docker
@@ -131,23 +130,6 @@ services:
- REDIS_PORT=6379
- REDIS_SSL=false
superset-checks:
build:
context: .
target: python-base
cache_from:
- apache/superset-cache:3.10-slim-bookworm
container_name: superset_checks
command: ["/app/scripts/check-env.py"]
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
user: *superset-user
healthcheck:
disable: true
superset-init:
build:
<<: *common-build
@@ -179,6 +161,7 @@ services:
# set this to false if you have perf issues running the npm i; npm run dev in-docker
# if you do so, you have to run this manually on the host, which should perform better!
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
NPM_RUN_PRUNE: false
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
container_name: superset_node
command: ["/app/docker/docker-frontend.sh"]

View File

@@ -27,6 +27,11 @@ if [ "$BUILD_SUPERSET_FRONTEND_IN_DOCKER" = "true" ]; then
echo "Building Superset frontend in dev mode inside docker container"
cd /app/superset-frontend
if [ "$NPM_RUN_PRUNE" = "true" ]; then
echo "Running `npm run prune`"
npm run prune
fi
echo "Running `npm install`"
npm install

View File

@@ -29,7 +29,7 @@ We have a set of build "presets" that each represent a combination of
parameters for the build, mostly pointing to either different target layer
for the build, and/or base image.
Here are the build presets that are exposed through the `build_docker.py` script:
Here are the build presets that are exposed through the `supersetbot docker` utility:
- `lean`: The default Docker image, including both frontend and backend. Tags
without a build_preset are lean builds (ie: `latest`, `4.0.0`, `3.0.0`, ...). `lean`
@@ -62,8 +62,8 @@ Here are the build presets that are exposed through the `build_docker.py` script
For insights or modifications to the build matrix and tagging conventions,
check the [build_docker.py](https://github.com/apache/superset/blob/master/scripts/build_docker.py)
script and the [docker.yml](https://github.com/apache/superset/blob/master/.github/workflows/docker.yml)
check the [supersetbot docker](https://github.com/apache-superset/supersetbot)
subcommand and the [docker.yml](https://github.com/apache/superset/blob/master/.github/workflows/docker.yml)
GitHub action.
## Key ARGs in Dockerfile

View File

@@ -95,6 +95,14 @@ perform those operations. In this case, we recommend you set the env var
Simply trigger `npm i && npm run dev`, this should be MUCH faster.
:::
:::tip
Sometimes, your npm-related state can get out-of-wack, running `npm run prune` from
the `superset-frontend/` folder will nuke the various' packages `node_module/` folders
and help you start fresh. In the context of `docker compose` setting
`export NPM_RUN_PRUNE=true` prior to running `docker compose up` will trigger that
from within docker. This will slow down the startup, but will fix various npm-related issues.
:::
### Option #2 - build a set of immutable images from the local branch
```bash

View File

@@ -77,10 +77,6 @@ versions officially supported by Superset. We'd recommend using a Python version
like [pyenv](https://github.com/pyenv/pyenv)
(and also [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)).
:::tip
To identify the Python version used by the official docker image, see the [Dockerfile](https://github.com/apache/superset/blob/master/Dockerfile). Additional docker images published for newer versions of Python can be found in [this file](https://github.com/apache/superset/blob/master/scripts/build_docker.py).
:::
Let's also make sure we have the latest version of `pip` and `setuptools`:
```bash

View File

@@ -27,33 +27,34 @@ following information about each flight is given:
You may need to enable the functionality to upload a CSV or Excel file to your database. The following section
explains how to enable this functionality for the examples database.
In the top menu, select **Data ‣ Databases**. Find the **examples** database in the list and
In the top menu, select **Settings ‣ Data ‣ Database Connections**. Find the **examples** database in the list and
select the **Edit** button.
<img src={useBaseUrl("/img/tutorial/edit-record.png" )} />
In the resulting modal window, switch to the **Extra** tab and
tick the checkbox for **Allow Data Upload**. End by clicking the **Save** button.
In the resulting modal window, switch to the **Advanced** tab and open **Security** section.
Then, tick the checkbox for **Allow file uploads to database**. End by clicking the **Finish** button.
<img src={useBaseUrl("/img/tutorial/add-data-upload.png" )} />
<img src={useBaseUrl("/img/tutorial/allow-file-uploads.png" )} />
### Loading CSV Data
Download the CSV dataset to your computer from
[GitHub](https://raw.githubusercontent.com/apache-superset/examples-data/master/tutorial_flights.csv).
In the Superset menu, select **Data ‣ Upload a CSV**.
In the top menu, select **Settings ‣ Data ‣ Database Connections**. Then, **Upload file to database ‣ Upload CSV**.
<img src={useBaseUrl("/img/tutorial/upload_a_csv.png" )} />
Then, enter the **Table Name** as _tutorial_flights_ and select the CSV file from your computer.
Then, select select the CSV file from your computer, select **Database** and **Schema**, and enter the **Table Name**
as _tutorial_flights_.
<img src={useBaseUrl("/img/tutorial/csv_to_database_configuration.png" )} />
Next enter the text _Travel Date_ into the **Parse Dates** field.
Next enter the text _Travel Date_ into the **File settings ‣ Columns to be parsed as dates** field.
<img src={useBaseUrl("/img/tutorial/parse_dates_column.png" )} />
Leaving all the other options in their default settings, select **Save** at the bottom of the page.
Leaving all the other options in their default settings, select **Upload** at the bottom of the page.
### Table Visualization

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -17,12 +17,12 @@ apsw==3.46.0.0
# via shillelagh
async-timeout==4.0.3
# via redis
attrs==23.2.0
attrs==24.2.0
# via
# cattrs
# jsonschema
# requests-cache
babel==2.15.0
babel==2.16.0
# via flask-babel
backoff==2.2.1
# via apache-superset
@@ -30,9 +30,9 @@ bcrypt==4.1.3
# via paramiko
billiard==4.2.0
# via celery
blinker==1.8.2
blinker==1.9.0
# via flask
bottleneck==1.3.8
bottleneck==1.4.2
# via pandas
brotli==1.1.0
# via flask-compress
@@ -42,13 +42,13 @@ cachelib==0.9.0
# flask-session
cachetools==5.3.3
# via google-auth
cattrs==23.2.3
cattrs==24.1.2
# via requests-cache
celery==5.4.0
# via apache-superset
certifi==2024.2.2
# via requests
cffi==1.16.0
cffi==1.17.1
# via
# cryptography
# pynacl
@@ -195,7 +195,7 @@ markdown==3.6
# via apache-superset
markdown-it-py==3.0.0
# via rich
markupsafe==2.1.5
markupsafe==3.0.2
# via
# jinja2
# mako
@@ -217,7 +217,7 @@ nh3==0.2.17
# via apache-superset
numba==0.59.1
# via pandas
numexpr==2.10.0
numexpr==2.10.1
# via
# -r requirements/base.in
# pandas
@@ -299,7 +299,7 @@ python-dotenv==1.0.1
# via apache-superset
python-geohash==0.8.5
# via apache-superset
pytz==2024.1
pytz==2024.2
# via
# croniter
# flask-babel
@@ -358,7 +358,7 @@ sshtunnel==0.4.0
# via apache-superset
tabulate==0.8.10
# via apache-superset
typing-extensions==4.12.0
typing-extensions==4.12.2
# via
# alembic
# apache-superset
@@ -385,7 +385,7 @@ vine==5.1.0
# kombu
wcwidth==0.2.13
# via prompt-toolkit
werkzeug==3.0.6
werkzeug==3.1.3
# via
# -r requirements/base.in
# flask

View File

@@ -1,294 +0,0 @@
#!/usr/bin/env python3
# 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 os
import re
import subprocess
from textwrap import dedent
import click
REPO = "apache/superset"
CACHE_REPO = f"{REPO}-cache"
BASE_PY_IMAGE = "3.10-slim-bookworm"
def run_cmd(command: str, raise_on_failure: bool = True) -> str:
process = subprocess.Popen(
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
output = ""
if process.stdout is not None:
for line in iter(process.stdout.readline, ""):
print(line.strip()) # Print the line to stdout in real-time
output += line
process.wait() # Wait for the subprocess to finish
if process.returncode != 0 and raise_on_failure:
raise subprocess.CalledProcessError(process.returncode, command, output)
return output
def get_git_sha() -> str:
return run_cmd("git rev-parse HEAD").strip()
def get_build_context_ref(build_context: str) -> str:
"""
Given a context, return a ref:
- if context is pull_request, return the PR's id
- if context is push, return the branch
- if context is release, return the release ref
"""
event = os.getenv("GITHUB_EVENT_NAME")
github_ref = os.getenv("GITHUB_REF", "")
if event == "pull_request":
github_head_ref = os.getenv("GITHUB_HEAD_REF", "")
return re.sub("[^a-zA-Z0-9]", "-", github_head_ref)[:40]
elif event == "release":
return re.sub("refs/tags/", "", github_ref)[:40]
elif event == "push":
return re.sub("[^a-zA-Z0-9]", "-", re.sub("refs/heads/", "", github_ref))[:40]
return ""
def is_latest_release(release: str) -> bool:
output = (
run_cmd(
f"./scripts/tag_latest_release.sh {release} --dry-run",
raise_on_failure=False,
)
or ""
)
return "SKIP_TAG::false" in output
def make_docker_tag(l: list[str]) -> str: # noqa: E741
return f"{REPO}:" + "-".join([o for o in l if o])
def get_docker_tags(
build_preset: str,
build_platforms: list[str],
sha: str,
build_context: str,
build_context_ref: str,
force_latest: bool = False,
) -> set[str]:
"""
Return a set of tags given a given build context
"""
tags: set[str] = set()
tag_chunks: list[str] = []
is_latest = is_latest_release(build_context_ref)
if build_preset != "lean":
# Always add the preset_build name if different from default (lean)
tag_chunks += [build_preset]
if len(build_platforms) == 1:
build_platform = build_platforms[0]
short_build_platform = build_platform.replace("linux/", "").replace("64", "")
if short_build_platform != "amd":
# Always a platform indicator if different from default (amd)
tag_chunks += [short_build_platform]
# Always craft a tag for the SHA
tags.add(make_docker_tag([sha] + tag_chunks))
# also a short SHA, cause it's nice
tags.add(make_docker_tag([sha[:7]] + tag_chunks))
if build_context == "release":
# add a release tag
tags.add(make_docker_tag([build_context_ref] + tag_chunks))
if is_latest or force_latest:
# add a latest tag
tags.add(make_docker_tag(["latest"] + tag_chunks))
elif build_context == "push" and build_context_ref == "master":
tags.add(make_docker_tag(["master"] + tag_chunks))
elif build_context == "pull_request":
tags.add(make_docker_tag([f"pr-{build_context_ref}"] + tag_chunks))
return tags
def get_docker_command(
build_preset: str,
build_platforms: list[str],
is_authenticated: bool,
sha: str,
build_context: str,
build_context_ref: str,
force_latest: bool = False,
) -> str:
tag = "" # noqa: F841
build_target = ""
py_ver = BASE_PY_IMAGE
docker_context = "."
if build_preset == "dev":
build_target = "dev"
elif build_preset == "lean":
build_target = "lean"
elif build_preset == "py311":
build_target = "lean"
py_ver = "3.11-slim-bookworm"
elif build_preset == "websocket":
build_target = ""
docker_context = "superset-websocket"
elif build_preset == "ci":
build_target = "ci"
elif build_preset == "dockerize":
build_target = ""
docker_context = "-f dockerize.Dockerfile ."
else:
print(f"Invalid build preset: {build_preset}")
exit(1)
# Try to get context reference if missing
if not build_context_ref:
build_context_ref = get_build_context_ref(build_context)
tags = get_docker_tags(
build_preset,
build_platforms,
sha,
build_context,
build_context_ref,
force_latest,
)
docker_tags = ("\\\n" + 8 * " ").join([f"-t {s} " for s in tags])
docker_args = "--load" if not is_authenticated else "--push"
target_argument = f"--target {build_target}" if build_target else ""
cache_ref = f"{CACHE_REPO}:{py_ver}"
if len(build_platforms) == 1:
build_platform = build_platforms[0]
short_build_platform = build_platform.replace("linux/", "").replace("64", "")
cache_ref = f"{CACHE_REPO}:{py_ver}-{short_build_platform}"
platform_arg = "--platform " + ",".join(build_platforms)
cache_from_arg = f"--cache-from=type=registry,ref={cache_ref}"
cache_to_arg = (
f"--cache-to=type=registry,mode=max,ref={cache_ref}" if is_authenticated else ""
)
build_arg = f"--build-arg PY_VER={py_ver}" if py_ver else ""
actor = os.getenv("GITHUB_ACTOR")
return dedent(
f"""\
docker buildx build \\
{docker_args} \\
{docker_tags} \\
{cache_from_arg} \\
{cache_to_arg} \\
{build_arg} \\
{platform_arg} \\
{target_argument} \\
--label sha={sha} \\
--label target={build_target} \\
--label build_trigger={build_context} \\
--label base={py_ver} \\
--label build_actor={actor} \\
{docker_context}"""
)
@click.command()
@click.argument(
"build_preset",
type=click.Choice(["lean", "dev", "dockerize", "websocket", "py311", "ci"]),
)
@click.argument("build_context", type=click.Choice(["push", "pull_request", "release"]))
@click.option(
"--platform",
type=click.Choice(["linux/arm64", "linux/amd64"]),
default=["linux/amd64"],
multiple=True,
)
@click.option("--build_context_ref", help="a reference to the pr, release or branch")
@click.option("--dry-run", is_flag=True, help="Run the command in dry-run mode.")
@click.option("--verbose", is_flag=True, help="Print more info")
@click.option(
"--force-latest", is_flag=True, help="Force the 'latest' tag on the release"
)
def main(
build_preset: str,
build_context: str,
build_context_ref: str,
platform: list[str],
dry_run: bool,
force_latest: bool,
verbose: bool,
) -> None:
"""
This script executes docker build and push commands based on given arguments.
"""
is_authenticated = (
True if os.getenv("DOCKERHUB_TOKEN") and os.getenv("DOCKERHUB_USER") else False
)
if force_latest and build_context != "release":
print(
"--force-latest can only be applied if the build context is set to 'release'"
)
exit(1)
if build_context == "release" and not build_context_ref.strip():
print("Release number has to be provided")
exit(1)
docker_build_command = get_docker_command(
build_preset,
platform,
is_authenticated,
get_git_sha(),
build_context,
build_context_ref,
force_latest,
)
if not dry_run:
print("Executing Docker Build Command:")
print(docker_build_command)
script = ""
if os.getenv("DOCKERHUB_USER"):
script = dedent(
f"""\
docker logout
docker login --username "{os.getenv("DOCKERHUB_USER")}" --password "{os.getenv("DOCKERHUB_TOKEN")}"
DOCKER_ARGS="--push"
"""
)
script = script + docker_build_command
if verbose:
run_cmd("cat Dockerfile")
stdout = run_cmd(script) # noqa: F841
else:
print("Dry Run - Docker Build Command:")
print(docker_build_command)
if __name__ == "__main__":
main()

View File

@@ -27,6 +27,11 @@ using your app's authentication.
Embedding is done by inserting an iframe, containing a Superset page, into the host application.
## Prerequisites
* Activate the feature flag `EMBEDDED_SUPERSET`
* Set a strong password in configuration variable `GUEST_TOKEN_JWT_SECRET` (see configuration file config.py). Be aware that its default value must be changed in production.
## Embedding a Dashboard
Using npm:
@@ -108,6 +113,30 @@ Example `POST /security/guest_token` payload:
]
}
```
Alternatively, a guest token can be created directly in your app with a json like the following, and then signed
with the secret set in configuration variable `GUEST_TOKEN_JWT_SECRET` (see configuration file config.py)
```
{
"user": {
"username": "embedded@embedded.fr",
"first_name": "embedded",
"last_name": "embedded"
},
"resources": [
{
"type": "dashboard",
"id": "d73e7841-9342-4afd-8e29-b4a416a2498c"
}
],
"rls_rules": [],
"iat": 1730883214,
"exp": 1732956814,
"aud": "superset",
"type": "guest"
}
```
### Sandbox iframe
The Embedded SDK creates an iframe with [sandbox](https://developer.mozilla.org/es/docs/Web/HTML/Element/iframe#sandbox) mode by default

View File

@@ -16,10 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SAMPLE_DASHBOARD_1, TABBED_DASHBOARD } from 'cypress/utils/urls';
import {
SAMPLE_DASHBOARD_1,
SUPPORTED_CHARTS_DASHBOARD,
TABBED_DASHBOARD,
} from 'cypress/utils/urls';
import { drag, resize, waitForChartLoad } from 'cypress/utils';
import * as ace from 'brace';
import { interceptGet, interceptUpdate, openTab } from './utils';
import {
interceptExploreUpdate,
interceptGet,
interceptUpdate,
openTab,
} from './utils';
import {
interceptExploreJson,
interceptFiltering as interceptCharts,
@@ -42,15 +51,37 @@ function openProperties() {
cy.getBySel('header-actions-menu')
.contains('Edit properties')
.click({ force: true });
cy.wait(500);
cy.get('.ant-modal-body').should('be.visible');
});
}
function openExploreProperties() {
cy.getBySel('actions-trigger').click({ force: true });
cy.get('.ant-dropdown-menu')
.contains('Edit chart properties')
.click({ force: true });
cy.get('.ant-modal-body').should('be.visible');
}
function assertMetadata(text: string) {
const regex = new RegExp(text);
cy.get('#json_metadata')
.should('be.visible')
.then(() => {
const metadata = cy.$$('#json_metadata')[0];
// cypress can read this locally, but not in ci
// so we have to use the ace module directly to fetch the value
expect(ace.edit(metadata).getValue()).to.match(regex);
});
}
function openAdvancedProperties() {
cy.get('.ant-modal-body')
.contains('Advanced')
.should('be.visible')
.click({ force: true });
cy.get('#json_metadata').should('be.visible');
}
function dragComponent(
@@ -83,20 +114,36 @@ function visitEdit(sampleDashboard = SAMPLE_DASHBOARD_1) {
cy.visit(sampleDashboard);
cy.wait('@get');
editDashboard();
cy.get('.grid-container').should('exist');
cy.wait('@filtering');
cy.wait(500);
}
function resetTabbedDashboard(go = false) {
function visit(sampleDashboard = SAMPLE_DASHBOARD_1) {
interceptCharts();
interceptGet();
if (sampleDashboard === SAMPLE_DASHBOARD_1) {
cy.createSampleDashboards([0]);
}
cy.visit(sampleDashboard);
cy.wait('@get');
cy.get('.grid-container').should('exist');
cy.wait(500);
}
function resetDashboardColors(dashboard = 'tabbed_dash') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cy.getDashboard('tabbed_dash').then((r: Record<string, any>) => {
cy.getDashboard(dashboard).then((r: Record<string, any>) => {
const jsonMetadata = r?.json_metadata || '{}';
const metadata = JSON.parse(jsonMetadata);
const resetMetadata = JSON.stringify({
...metadata,
color_scheme: '',
label_colors: {},
shared_label_colors: {},
shared_label_colors: [],
map_label_colors: {},
});
cy.updateDashboard(r.id, {
certification_details: r.certification_details,
@@ -106,25 +153,35 @@ function resetTabbedDashboard(go = false) {
json_metadata: resetMetadata,
owners: r.owners,
slug: r.slug,
}).then(() => {
if (go) {
visitEdit(TABBED_DASHBOARD);
}
});
});
}
function visitResetTabbedDashboard() {
resetTabbedDashboard(true);
function selectColorScheme(
color: string,
target = 'dashboard-edit-properties-form',
) {
cy.get(`[data-test="${target}"] input[aria-label="Select color scheme"]`)
.first()
.then($input => {
cy.wrap($input).click({ force: true });
cy.wrap($input).type(color.slice(0, 5), { force: true });
});
cy.getBySel(color).click({ force: true });
}
function selectColorScheme(color: string) {
cy.get(
'[data-test="dashboard-edit-properties-form"] [aria-label="Select color scheme"]',
)
.first()
.click();
cy.getBySel(color).click({ force: true });
function saveAndGo(dashboard = 'Tabbed Dashboard') {
interceptExploreUpdate();
cy.getBySel('query-save-button').click();
cy.getBySel('save-modal-body').then($modal => {
cy.wrap($modal)
.find("div[aria-label='Select a dashboard'] .ant-select-selection-item")
.should('have.text', dashboard);
cy.getBySel('save-overwrite-radio').should('not.be.disabled');
cy.getBySel('save-overwrite-radio').click();
cy.get('#btn_modal_save_goto_dash').click();
cy.wait('@chartUpdate');
});
}
function applyChanges() {
@@ -137,37 +194,37 @@ function saveChanges() {
cy.wait('@update');
}
function assertMetadata(text: string) {
const regex = new RegExp(text);
cy.get('#json_metadata')
.should('be.visible')
.then(() => {
const metadata = cy.$$('#json_metadata')[0];
// cypress can read this locally, but not in ci
// so we have to use the ace module directly to fetch the value
expect(ace.edit(metadata).getValue()).to.match(regex);
});
}
function clearMetadata() {
cy.get('#json_metadata').then($jsonmetadata => {
cy.wrap($jsonmetadata).find('.ace_content').click();
cy.wrap($jsonmetadata).find('.ace_content').click({ force: true });
cy.wrap($jsonmetadata)
.find('.ace_text-input')
.type('{selectall} {backspace}', { force: true });
.then($ace => {
cy.wrap($ace).focus();
cy.wrap($ace).should('have.focus');
cy.wrap($ace).type('{selectall}', { force: true });
cy.wrap($ace).type('{backspace}', { force: true });
});
});
}
function writeMetadata(metadata: string) {
cy.get('#json_metadata').then($jsonmetadata =>
cy
.wrap($jsonmetadata)
cy.get('#json_metadata').then($jsonmetadata => {
cy.wrap($jsonmetadata).find('.ace_content').click({ force: true });
cy.wrap($jsonmetadata)
.find('.ace_text-input')
.type(metadata, { parseSpecialCharSequences: false, force: true }),
);
.then($ace => {
cy.wrap($ace).focus();
cy.wrap($ace).should('have.focus');
cy.wrap($ace).type(metadata, {
parseSpecialCharSequences: false,
force: true,
});
});
});
}
function openExplore(chartName: string) {
function openExploreWithDashboardContext(chartName: string) {
interceptExploreJson();
interceptGet();
@@ -181,23 +238,351 @@ function openExplore(chartName: string) {
.should('contain', 'Edit chart')
.click();
cy.wait('@getJson');
cy.get('.chart-container').should('exist');
}
function saveExploreColorScheme(
chart = 'Top 10 California Names Timeseries',
colorScheme = 'supersetColors',
) {
interceptExploreUpdate();
openExploreWithDashboardContext(chart);
openTab(0, 1, 'control-tabs');
selectColorScheme(colorScheme, 'control-item');
cy.getBySel('query-save-button').click();
cy.getBySel('save-overwrite-radio').click();
cy.getBySel('btn-modal-save').click();
cy.wait('@chartUpdate');
}
describe('Dashboard edit', () => {
describe('Color consistency', () => {
beforeEach(() => {
visitResetTabbedDashboard();
resetDashboardColors();
});
after(() => {
resetTabbedDashboard();
it('should not allow to change color scheme of a chart when dashboard has one', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
saveChanges();
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
openTab(0, 1, 'control-tabs');
cy.get('[aria-label="Select color scheme"]').should('be.disabled');
});
it('should not allow to change color scheme of a chart when dashboard has no scheme but chart has shared labels', () => {
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// open second top tab to catch shared labels
openTab(0, 1);
waitForChartLoad({
name: 'Trends',
viz: 'line',
});
openTab(0, 0);
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
openTab(0, 1, 'control-tabs');
cy.get('[aria-label="Select color scheme"]').should('be.disabled');
});
it('should allow to change color scheme of a chart when dashboard has no scheme but only custom label colors', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
writeMetadata('{"color_scheme":"","label_colors":{"Anthony":"red"}}');
applyChanges();
saveChanges();
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openTab(0, 1, 'control-tabs');
selectColorScheme('blueToGreen', 'control-item');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
// label Christopher
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// label Daniel
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(0, 76, 218)');
// label David
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(3)
.should('have.css', 'fill', 'rgb(0, 116, 241)');
});
it('should allow to change color scheme of a chart when dashboard has no scheme and show the change', () => {
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
openTab(0, 1, 'control-tabs');
selectColorScheme('blueToGreen', 'control-item');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
saveAndGo();
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// reset original scheme
saveExploreColorScheme();
});
it('should allow to change color scheme of a chart when dashboard has no scheme but custom label colors and show the change', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
writeMetadata('{"color_scheme":"","label_colors":{"Anthony":"red"}}');
applyChanges();
saveChanges();
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openTab(0, 1, 'control-tabs');
selectColorScheme('blueToGreen', 'control-item');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
// label Christopher
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
saveAndGo();
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
// label Christopher
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// reset original scheme
saveExploreColorScheme();
});
it('should not change colors on refreshes with no color scheme set', () => {
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Andrew
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(69, 78, 124)');
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Andrew
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(69, 78, 124)');
});
it('should not change colors on refreshes with color scheme set', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
saveChanges();
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Andrew
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(0, 76, 218)');
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Andrew
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(0, 76, 218)');
});
it('should respect chart color scheme when none is set for the dashboard', () => {
openProperties();
cy.get('[aria-label="Select color scheme"]').should('have.value', '');
applyChanges();
saveChanges();
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
@@ -214,7 +599,8 @@ describe('Dashboard edit', () => {
.should('have.css', 'fill', 'rgb(31, 168, 201)');
});
it('should apply same color to same labels with color scheme set', () => {
it('should apply same color to same labels with color scheme set on refresh', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@@ -238,17 +624,63 @@ describe('Dashboard edit', () => {
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
});
it('should apply same color to same labels with no color scheme set', () => {
openProperties();
cy.get('[aria-label="Select color scheme"]').should('have.value', '');
applyChanges();
saveChanges();
it('should apply same color to same labels with no color scheme set on refresh', () => {
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(31, 168, 201)');
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
@@ -275,6 +707,7 @@ describe('Dashboard edit', () => {
});
it('custom label colors should take the precedence in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
@@ -305,6 +738,7 @@ describe('Dashboard edit', () => {
});
it('label colors should take the precedence for rendered charts in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
// open the tab first time and let chart load
openTab(1, 1);
waitForChartLoad({
@@ -333,6 +767,7 @@ describe('Dashboard edit', () => {
});
it('should re-apply original color after removing custom label color with color scheme set', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
@@ -375,6 +810,7 @@ describe('Dashboard edit', () => {
});
it('should re-apply original color after removing custom label color with no color scheme set', () => {
visitEdit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
@@ -438,6 +874,7 @@ describe('Dashboard edit', () => {
});
it('should show the same colors in Explore', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
@@ -461,7 +898,7 @@ describe('Dashboard edit', () => {
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openExplore('Top 10 California Names Timeseries');
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
@@ -469,7 +906,8 @@ describe('Dashboard edit', () => {
.should('have.css', 'fill', 'rgb(255, 0, 0)');
});
it.skip('should change color scheme multiple times', () => {
it('should change color scheme multiple times', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@@ -487,7 +925,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open 2nd main tab
openTab(0, 1);
@@ -496,7 +934,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
editDashboard();
openProperties();
@@ -507,7 +945,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(41, 105, 107)');
.should('have.css', 'fill', 'rgb(0, 128, 246)');
// open main tab and nested tab
openTab(0, 0);
@@ -518,10 +956,11 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(41, 105, 107)');
.should('have.css', 'fill', 'rgb(0, 128, 246)');
});
it.skip('should apply the color scheme across main tabs', () => {
it('should apply the color scheme across main tabs', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@@ -533,10 +972,11 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
});
it.skip('should apply the color scheme across main tabs for rendered charts', () => {
it('should apply the color scheme across main tabs for rendered charts', () => {
visitEdit(TABBED_DASHBOARD);
waitForChartLoad({ name: 'Treemap', viz: 'treemap_v2' });
openProperties();
selectColorScheme('blueToGreen');
@@ -549,7 +989,7 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(41, 105, 107)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// change scheme now that charts are rendered across the main tabs
editDashboard();
@@ -560,10 +1000,11 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(0, 128, 246)');
});
it.skip('should apply the color scheme in nested tabs', () => {
it('should apply the color scheme in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@@ -579,17 +1020,18 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open another nested tab
openTab(2, 1);
waitForChartLoad({ name: 'Growth Rate', viz: 'line' });
cy.get('[data-test-chart-name="Growth Rate"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
});
it.skip('should apply a valid color scheme for rendered charts in nested tabs', () => {
it('should apply a valid color scheme for rendered charts in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
// open the tab first time and let chart load
openTab(1, 1);
waitForChartLoad({
@@ -611,7 +1053,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
});
});
@@ -623,9 +1065,10 @@ describe('Dashboard edit', () => {
beforeEach(() => {
cy.createSampleDashboards([0]);
openProperties();
selectColorScheme('supersetColors');
});
it.skip('should accept a valid color scheme', () => {
it('should accept a valid color scheme', () => {
openAdvancedProperties();
clearMetadata();
writeMetadata('{"color_scheme":"lyftColors"}');
@@ -636,21 +1079,21 @@ describe('Dashboard edit', () => {
applyChanges();
});
it.skip('should overwrite the color scheme when advanced is closed', () => {
it('should overwrite the color scheme when advanced is closed', () => {
selectColorScheme('blueToGreen');
openAdvancedProperties();
assertMetadata('blueToGreen');
applyChanges();
});
it.skip('should overwrite the color scheme when advanced is open', () => {
it('should overwrite the color scheme when advanced is open', () => {
openAdvancedProperties();
selectColorScheme('modernSunset');
assertMetadata('modernSunset');
applyChanges();
});
it.skip('should not accept an invalid color scheme', () => {
it('should not accept an invalid color scheme', () => {
openAdvancedProperties();
clearMetadata();
// allow console error
@@ -716,7 +1159,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);
@@ -765,7 +1208,7 @@ describe('Dashboard edit', () => {
visitEdit();
});
it.skip('should save', () => {
it('should save', () => {
cy.get('[role="checkbox"]').click();
dragComponent();
cy.getBySel('header-save-button').should('be.enabled');

View File

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

View File

@@ -125,7 +125,7 @@ export const valueNativeFilterOptions = [
];
export function interceptGet() {
cy.intercept('/api/v1/dashboard/*').as('get');
cy.intercept('GET', '/api/v1/dashboard/*').as('get');
}
export function interceptFiltering() {
@@ -144,6 +144,10 @@ export function interceptUpdate() {
cy.intercept('PUT', `/api/v1/dashboard/*`).as('update');
}
export function interceptExploreUpdate() {
cy.intercept('PUT', `/api/v1/chart/*`).as('chartUpdate');
}
export function interceptPost() {
cy.intercept('POST', `/api/v1/dashboard/`).as('post');
}
@@ -524,13 +528,17 @@ export function addCountryNameFilter() {
);
}
export function openTab(tabComponentIndex: number, tabIndex: number) {
return cy
.getBySel('dashboard-component-tabs')
export function openTab(
tabComponentIndex: number,
tabIndex: number,
target = 'dashboard-component-tabs',
) {
cy.getBySel(target)
.eq(tabComponentIndex)
.find('[role="tab"]')
.eq(tabIndex)
.click();
cy.wait(500);
}
export const openTopLevelTab = (tabName: string) => {

View File

@@ -5920,7 +5920,9 @@
}
},
"node_modules/@jest/core/node_modules/micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6245,10 +6247,11 @@
}
},
"node_modules/@jest/transform/node_modules/micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
@@ -6686,10 +6689,11 @@
}
},
"node_modules/@lerna/create/node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -8001,10 +8005,11 @@
}
},
"node_modules/@npmcli/map-workspaces/node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -8214,10 +8219,11 @@
}
},
"node_modules/@npmcli/package-json/node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -10415,7 +10421,9 @@
}
},
"node_modules/@storybook/cli/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10705,7 +10713,9 @@
}
},
"node_modules/@storybook/codemod/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10905,7 +10915,9 @@
}
},
"node_modules/@storybook/core-common/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -12005,7 +12017,9 @@
}
},
"node_modules/@storybook/react-docgen-typescript-plugin/node_modules/micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -18730,7 +18744,9 @@
}
},
"node_modules/cacache/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -20586,9 +20602,9 @@
}
},
"node_modules/cross-env/node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -20650,7 +20666,9 @@
}
},
"node_modules/cross-spawn": {
"version": "6.0.5",
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
"integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -21083,7 +21101,9 @@
}
},
"node_modules/cypress/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -22061,7 +22081,9 @@
}
},
"node_modules/default-gateway/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -22490,7 +22512,9 @@
}
},
"node_modules/detect-package-manager/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -24823,7 +24847,9 @@
"license": "Python-2.0"
},
"node_modules/eslint/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -25824,9 +25850,10 @@
}
},
"node_modules/fast-glob/node_modules/micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
@@ -26546,7 +26573,9 @@
}
},
"node_modules/foreground-child/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -29053,11 +29082,13 @@
}
},
"node_modules/http-proxy-middleware/node_modules/micromatch": {
"version": "4.0.5",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.2",
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@@ -30432,7 +30463,9 @@
}
},
"node_modules/istanbul-lib-processinfo/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -30718,7 +30751,9 @@
}
},
"node_modules/jest-changed-files/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -31113,7 +31148,9 @@
}
},
"node_modules/jest-config/node_modules/micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -32276,10 +32313,11 @@
}
},
"node_modules/jest-haste-map/node_modules/micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
@@ -32498,7 +32536,9 @@
}
},
"node_modules/jest-message-util/node_modules/micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -33082,7 +33122,9 @@
}
},
"node_modules/jscodeshift/node_modules/micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -34052,7 +34094,9 @@
}
},
"node_modules/lerna/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -40872,10 +40916,11 @@
}
},
"node_modules/nise/node_modules/path-to-regexp": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz",
"integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==",
"dev": true
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
"dev": true,
"license": "MIT"
},
"node_modules/no-case": {
"version": "3.0.4",
@@ -41019,10 +41064,11 @@
}
},
"node_modules/node-gyp/node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -42067,7 +42113,9 @@
}
},
"node_modules/nypm/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -46425,7 +46473,9 @@
"license": "MIT"
},
"node_modules/react-router/node_modules/path-to-regexp": {
"version": "1.8.0",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
"license": "MIT",
"dependencies": {
"isarray": "0.0.1"
@@ -48702,7 +48752,9 @@
}
},
"node_modules/rimraf/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -51905,15 +51957,17 @@
}
},
"node_modules/ts-loader/node_modules/micromatch": {
"version": "4.0.2",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8"
"node": ">=8.6"
}
},
"node_modules/ts-loader/node_modules/semver": {
@@ -53400,7 +53454,9 @@
}
},
"node_modules/webpack-cli/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -54046,7 +54102,9 @@
}
},
"node_modules/which-package-manager/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -54168,7 +54226,9 @@
}
},
"node_modules/which-package-manager/node_modules/micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -55144,7 +55204,9 @@
}
},
"packages/generator-superset/node_modules/cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -55639,7 +55701,9 @@
}
},
"packages/generator-superset/node_modules/path-to-regexp": {
"version": "6.2.2",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
"dev": true,
"license": "MIT"
},
@@ -57504,12 +57568,14 @@
"license": "MIT"
},
"packages/superset-ui-demo/node_modules/micromatch": {
"version": "4.0.4",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.1",
"picomatch": "^2.2.3"
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
@@ -62473,7 +62539,9 @@
"dev": true
},
"micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.3",
@@ -62709,9 +62777,9 @@
"dev": true
},
"micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.3",
@@ -63053,9 +63121,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -64047,9 +64115,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"devOptional": true,
"requires": {
"path-key": "^3.1.0",
@@ -64198,9 +64266,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"devOptional": true,
"requires": {
"path-key": "^3.1.0",
@@ -65617,7 +65685,9 @@
"dev": true
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -65776,7 +65846,9 @@
"dev": true
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -65923,7 +65995,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -66599,7 +66673,9 @@
}
},
"micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.3",
@@ -67473,11 +67549,13 @@
"version": "6.0.0"
},
"micromatch": {
"version": "4.0.4",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.2.3"
"braces": "^3.0.3",
"picomatch": "^2.3.1"
}
},
"p-limit": {
@@ -67848,7 +67926,9 @@
"peer": true
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -68124,7 +68204,9 @@
"version": "3.1.1"
},
"path-to-regexp": {
"version": "6.2.2",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
"dev": true
},
"path-type": {
@@ -73623,7 +73705,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"devOptional": true,
"requires": {
"path-key": "^3.1.0",
@@ -74872,9 +74956,9 @@
},
"dependencies": {
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -74915,7 +74999,9 @@
}
},
"cross-spawn": {
"version": "6.0.5",
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
"integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
"dev": true,
"requires": {
"nice-try": "^1.0.4",
@@ -75195,7 +75281,9 @@
"peer": true
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"peer": true,
"requires": {
@@ -75882,7 +75970,9 @@
},
"dependencies": {
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -76152,7 +76242,9 @@
},
"dependencies": {
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -77222,7 +77314,9 @@
"dev": true
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -78326,9 +78420,9 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"requires": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
@@ -78827,7 +78921,9 @@
},
"dependencies": {
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"peer": true,
"requires": {
@@ -80440,10 +80536,12 @@
"dev": true
},
"micromatch": {
"version": "4.0.5",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.2",
"braces": "^3.0.3",
"picomatch": "^2.3.1"
}
},
@@ -81290,7 +81388,9 @@
},
"dependencies": {
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"peer": true,
"requires": {
@@ -81464,7 +81564,9 @@
},
"dependencies": {
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -81696,7 +81798,9 @@
"dev": true
},
"micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.3",
@@ -82489,9 +82593,9 @@
"dev": true
},
"micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.3",
@@ -82635,7 +82739,9 @@
"dev": true
},
"micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.3",
@@ -83042,7 +83148,9 @@
"dev": true
},
"micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.3",
@@ -83730,7 +83838,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -87511,9 +87621,9 @@
},
"dependencies": {
"path-to-regexp": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz",
"integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
"dev": true
}
}
@@ -87616,9 +87726,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"devOptional": true,
"requires": {
"path-key": "^3.1.0",
@@ -88350,7 +88460,9 @@
},
"dependencies": {
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -91062,7 +91174,9 @@
"version": "0.0.1"
},
"path-to-regexp": {
"version": "1.8.0",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
"requires": {
"isarray": "0.0.1"
}
@@ -92514,7 +92628,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -94657,11 +94773,13 @@
}
},
"micromatch": {
"version": "4.0.2",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
"braces": "^3.0.3",
"picomatch": "^2.3.1"
}
},
"semver": {
@@ -95625,7 +95743,9 @@
"dev": true
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@@ -96042,7 +96162,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"optional": true,
"peer": true,
"requires": {
@@ -96113,7 +96235,9 @@
}
},
"micromatch": {
"version": "4.0.7",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"optional": true,
"peer": true,
"requires": {

View File

@@ -50,6 +50,7 @@ import {
import {
formatSelectOptions,
displayTimeRelatedControls,
getColorControlsProps,
D3_FORMAT_OPTIONS,
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
@@ -142,9 +143,7 @@ const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
renderTrigger: true,
schemes: () => sequentialSchemeRegistry.getMap(),
isLinear: true,
mapStateToProps: state => ({
dashboardId: state?.form_data?.dashboardId,
}),
mapStateToProps: state => getColorControlsProps(state),
};
const granularity: SharedControlConfig<'SelectControl'> = {
@@ -333,9 +332,7 @@ const color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
choices: () => categoricalSchemeRegistry.keys().map(s => [s, s]),
description: t('The color scheme for rendering chart'),
schemes: () => categoricalSchemeRegistry.getMap(),
mapStateToProps: state => ({
dashboardId: state?.form_data?.dashboardId,
}),
mapStateToProps: state => getColorControlsProps(state),
};
const time_shift_color: SharedControlConfig<'CheckboxControl'> = {

View File

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

View File

@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const getColorControlsProps = (state: Record<string, any>) => {
const dashboardId = state?.form_data?.dashboardId;
return {
chartId: state?.slice?.slice_id,
dashboardId,
hasDashboardColorScheme:
!!dashboardId && !!state?.form_data?.dashboard_color_scheme,
hasCustomLabelsColor:
Object.keys(state?.form_data?.label_colors || {}).length > 0,
colorNamespace: state?.form_data?.color_namespace,
mapLabelsColors: state?.form_data?.map_label_colors || {},
sharedLabelsColors: state?.form_data?.shared_label_colors || [],
};
};

View File

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

View File

@@ -0,0 +1,112 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getColorControlsProps } from '../../src';
describe('getColorControlsProps', () => {
it('should return default values when state is empty', () => {
const state = {};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: undefined,
dashboardId: undefined,
hasDashboardColorScheme: false,
hasCustomLabelsColor: false,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
it('should return correct values when state has form_data with dashboardId and color scheme', () => {
const state = {
form_data: {
dashboardId: 123,
dashboard_color_scheme: 'blueScheme',
label_colors: {},
},
slice: { slice_id: 456 },
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: 456,
dashboardId: 123,
hasDashboardColorScheme: true,
hasCustomLabelsColor: false,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
it('should detect custom label colors correctly', () => {
const state = {
form_data: {
dashboardId: 123,
label_colors: { label1: '#000000' },
},
slice: { slice_id: 456 },
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: 456,
dashboardId: 123,
hasDashboardColorScheme: false,
hasCustomLabelsColor: true,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
it('should return shared label colors when available', () => {
const state = {
form_data: {
shared_label_colors: ['#FF5733', '#33FF57'],
},
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: undefined,
dashboardId: undefined,
hasDashboardColorScheme: false,
hasCustomLabelsColor: false,
sharedLabelsColors: ['#FF5733', '#33FF57'],
colorNamespace: undefined,
mapLabelsColors: {},
});
});
it('should handle missing form_data and slice properties', () => {
const state = {
form_data: {
dashboardId: 789,
},
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: undefined,
dashboardId: 789,
hasDashboardColorScheme: false,
hasCustomLabelsColor: false,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
});

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import { cloneDeep } from 'lodash';
import CategoricalColorScale from './CategoricalColorScale';
import { ColorsLookup } from './types';
import getCategoricalSchemeRegistry from './CategoricalSchemeRegistrySingleton';
@@ -37,10 +38,21 @@ export default class CategoricalColorNamespace {
this.forcedItems = {};
}
getScale(schemeId?: string) {
const id = schemeId ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
/**
* A new CategoricalColorScale instance is created for each chart.
*
* @param colorScheme - the color scheme to use
* @returns a new instance of a color scale
*/
getScale(colorScheme?: string) {
const id =
colorScheme ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
const scheme = getCategoricalSchemeRegistry().get(id);
return new CategoricalColorScale(scheme?.colors ?? [], this.forcedItems);
return new CategoricalColorScale(
scheme?.colors ?? [],
this.forcedItems,
colorScheme,
);
}
/**
@@ -59,6 +71,17 @@ export default class CategoricalColorNamespace {
resetColors() {
this.forcedItems = {};
}
resetColorsForLabels(labels: string[] = []) {
const updatedForcedItems = cloneDeep(this.forcedItems);
labels.forEach(label => {
if (updatedForcedItems.hasOwnProperty(label)) {
delete updatedForcedItems[label];
}
});
this.forcedItems = { ...updatedForcedItems };
}
}
const namespaces: {
@@ -80,16 +103,19 @@ export function getNamespace(name: string = DEFAULT_NAMESPACE) {
export function getColor(
value?: string,
schemeId?: string,
colorScheme?: string,
namespace?: string,
) {
return getNamespace(namespace).getScale(schemeId).getColor(value);
return getNamespace(namespace).getScale(colorScheme).getColor(value);
}
/*
Returns a new scale instance within the same namespace.
Especially useful when a chart is booting for the first time
@param scheme - the applied color scheme
@param namespace - the namespace
*/
export function getScale(scheme?: string, namespace?: string) {
return getNamespace(namespace).getScale(scheme);
export function getScale(colorScheme?: string, namespace?: string) {
return getNamespace(namespace).getScale(colorScheme);
}

View File

@@ -21,14 +21,16 @@ import { scaleOrdinal, ScaleOrdinal } from 'd3-scale';
import { ExtensibleFunction } from '../models';
import { ColorsInitLookup, ColorsLookup } from './types';
import stringifyAndTrim from './stringifyAndTrim';
import getLabelsColorMap from './LabelsColorMapSingleton';
import getLabelsColorMap, {
LabelsColorMapSource,
} from './LabelsColorMapSingleton';
import { getAnalogousColors } from './utils';
import { FeatureFlag, isFeatureEnabled } from '../utils';
// Use type augmentation to correct the fact that
// an instance of CategoricalScale is also a function
interface CategoricalColorScale {
(x: { toString(): string }, y?: number, w?: string): string;
(x: { toString(): string }, y?: number): string;
}
class CategoricalColorScale extends ExtensibleFunction {
@@ -50,11 +52,16 @@ class CategoricalColorScale extends ExtensibleFunction {
* Constructor
* @param {*} colors an array of colors
* @param {*} forcedColors optional parameter that comes from parent
* (usually CategoricalColorNamespace)
* @param {*} appliedColorScheme the color scheme applied to the chart
*
*/
constructor(colors: string[], forcedColors: ColorsInitLookup = {}) {
super((value: string, sliceId?: number, colorScheme?: string) =>
this.getColor(value, sliceId, colorScheme),
constructor(
colors: string[],
forcedColors: ColorsInitLookup = {},
appliedColorScheme?: string,
) {
super((value: string, sliceId?: number) =>
this.getColor(value, sliceId, appliedColorScheme),
);
// holds original color scheme colors
this.originColors = colors;
@@ -107,15 +114,28 @@ class CategoricalColorScale extends ExtensibleFunction {
*
* @param value the value of a label to get the color for
* @param sliceId the ID of the current chart
* @param colorScheme the original color scheme of the chart
* @param appliedColorScheme the color scheme applied to the chart
* @returns the color or the next available color
*/
getColor(value?: string, sliceId?: number, colorScheme?: string): string {
getColor(
value?: string,
sliceId?: number,
appliedColorScheme?: string,
): string {
const cleanedValue = stringifyAndTrim(value);
// priority: forced color (i.e. custom label colors) > shared color > scale color
// priority: forced color (aka custom label colors) > shared color > scale color
const forcedColor = this.forcedColors?.[cleanedValue];
const isExistingLabel = this.chartLabelsColorMap.has(cleanedValue);
let color = forcedColor || this.scale(cleanedValue);
const { source } = this.labelsColorMapInstance;
const currentColorMap =
source === LabelsColorMapSource.Dashboard
? this.labelsColorMapInstance.getColorMap()
: this.chartLabelsColorMap;
const isExistingLabel = currentColorMap.has(cleanedValue);
let color =
forcedColor ||
(isExistingLabel
? (currentColorMap.get(cleanedValue) as string)
: this.scale(cleanedValue));
// a forced color will always be used independently of the usage count
if (!forcedColor && !isExistingLabel) {
@@ -128,7 +148,7 @@ class CategoricalColorScale extends ExtensibleFunction {
this.isColorUsed(color)
) {
// fallback to least used color
color = this.getNextAvailableColor(color);
color = this.getNextAvailableColor(cleanedValue, color);
}
}
@@ -141,7 +161,7 @@ class CategoricalColorScale extends ExtensibleFunction {
cleanedValue,
color,
sliceId,
colorScheme,
appliedColorScheme,
);
}
return color;
@@ -164,48 +184,76 @@ class CategoricalColorScale extends ExtensibleFunction {
* @param color the color to check
* @returns the count of the color usage in this slice
*/
getColorUsageCount(currentColor: string): number {
let count = 0;
this.chartLabelsColorMap.forEach(color => {
if (color === currentColor) {
count += 1;
}
});
return count;
getColorUsageCount(color: string): number {
return Array.from(this.chartLabelsColorMap.values()).filter(
value => value === color,
).length;
}
/**
* Lower chances of color collision by returning the least used color
* Checks across colors of current slice within LabelsColorMapSingleton
* Lower chances of color collision by returning the least used color.
* Checks across colors of current slice within chartLabelsColorMap.
*
* @param currentLabel the current label
* @param currentColor the current color
* @returns the least used color that is not the excluded color
* @returns the least used color that is not the current color
*/
getNextAvailableColor(currentColor: string) {
const colorUsageArray = this.colors.map(color => ({
color,
count: this.getColorUsageCount(color),
}));
const currentColorCount = this.getColorUsageCount(currentColor);
const otherColors = colorUsageArray.filter(
colorEntry => colorEntry.color !== currentColor,
);
// all other colors are used as much or more than currentColor
const hasNoneAvailable = otherColors.every(
colorEntry => colorEntry.count >= currentColorCount,
getNextAvailableColor(currentLabel: string, currentColor: string): string {
// Precompute color usage counts for all colors
const colorUsageCounts = new Map(
this.colors.map(color => [color, this.getColorUsageCount(color)]),
);
// fallback to currentColor color
if (!otherColors.length || hasNoneAvailable) {
return currentColor;
// Get an ordered array of labels from the map
const orderedLabels = Array.from(this.chartLabelsColorMap.keys());
const currentLabelIndex = orderedLabels.indexOf(currentLabel);
// Helper to infer "previous" and "next" labels based on index
const getAdjacentLabelsColors = (): string[] => {
const previousLabel =
currentLabelIndex > 0 ? orderedLabels[currentLabelIndex - 1] : null;
const nextLabel =
currentLabelIndex < orderedLabels.length - 1
? orderedLabels[currentLabelIndex + 1]
: null;
const previousColor = previousLabel
? this.chartLabelsColorMap.get(previousLabel)
: null;
const nextColor = nextLabel
? this.chartLabelsColorMap.get(nextLabel)
: null;
return [previousColor, nextColor].filter(color => color) as string[];
};
const adjacentColors = getAdjacentLabelsColors();
// Determine adjusted score (usage count + penalties)
const calculateScore = (color: string): number => {
/* istanbul ignore next */
const usageCount = colorUsageCounts.get(color) || 0;
const adjacencyPenalty = adjacentColors.includes(color) ? 100 : 0;
return usageCount + adjacencyPenalty;
};
// If there is any color that has never been used, prioritize it
const unusedColor = this.colors.find(
color => (colorUsageCounts.get(color) || 0) === 0,
);
if (unusedColor) {
return unusedColor;
}
// Finding the least used color
const leastUsedColor = otherColors.reduce((min, entry) =>
entry.count < min.count ? entry : min,
).color;
// If all colors are used, calculate scores and choose the best one
const otherColors = this.colors.filter(color => color !== currentColor);
return leastUsedColor;
// Find the color with the minimum score, defaulting to currentColor
return otherColors.reduce((bestColor, color) => {
const bestScore = calculateScore(bestColor);
const currentScore = calculateScore(color);
return currentScore < bestScore ? color : bestColor;
}, currentColor);
}
/**

View File

@@ -18,6 +18,7 @@
*/
import { makeSingleton } from '../utils';
import CategoricalColorNamespace from './CategoricalColorNamespace';
export enum LabelsColorMapSource {
Dashboard,
@@ -25,7 +26,10 @@ export enum LabelsColorMapSource {
}
export class LabelsColorMap {
chartsLabelsMap: Map<number, { labels: string[]; scheme?: string }>;
chartsLabelsMap: Map<
number,
{ labels: string[]; scheme?: string; ownScheme?: string }
>;
colorMap: Map<string, string>;
@@ -38,17 +42,38 @@ export class LabelsColorMap {
this.source = LabelsColorMapSource.Dashboard;
}
updateColorMap(categoricalNamespace: any, colorScheme?: string) {
const newColorMap = new Map();
this.colorMap.clear();
/**
* Wipes out the color map and updates it with the new color scheme.
*
* @param categoricalNamespace - the namespace to use for color mapping
* @param colorScheme - color scheme
*/
updateColorMap(
categoricalNamespace: CategoricalColorNamespace,
colorScheme?: string,
merge = false,
) {
const newColorMap = this.colorMap;
if (!merge) {
newColorMap.clear();
}
this.chartsLabelsMap.forEach((chartConfig, sliceId) => {
const { labels, scheme: originalChartColorScheme } = chartConfig;
const currentColorScheme = colorScheme || originalChartColorScheme;
const colorScale = categoricalNamespace.getScale(currentColorScheme);
const { labels, ownScheme } = chartConfig;
const appliedColorScheme = colorScheme || ownScheme;
const colorScale = categoricalNamespace.getScale(appliedColorScheme);
labels.forEach(label => {
const newColor = colorScale.getColor(label, sliceId);
newColorMap.set(label, newColor);
// if merge, apply the scheme only to new labels in the map
if (!merge || !this.colorMap.has(label)) {
const newColor = colorScale.getColor(
label,
sliceId,
appliedColorScheme,
);
newColorMap.set(label, newColor);
}
});
});
this.colorMap = newColorMap;
@@ -58,29 +83,63 @@ export class LabelsColorMap {
return this.colorMap;
}
/**
*
* Called individually by each plugin via getColor fn.
*
* @param label - the label name
* @param color - the color
* @param sliceId - the chart id
* @param colorScheme - the color scheme
*
*/
addSlice(
label: string,
color: string,
sliceId: number,
colorScheme?: string,
) {
if (this.source !== LabelsColorMapSource.Dashboard) return;
const chartConfig = this.chartsLabelsMap.get(sliceId) || {
labels: [],
scheme: '',
scheme: undefined,
ownScheme: undefined,
};
const { labels } = chartConfig;
if (!labels.includes(label)) {
labels.push(label);
this.chartsLabelsMap.set(sliceId, {
labels,
scheme: colorScheme,
ownScheme: chartConfig.ownScheme,
});
}
this.colorMap.set(label, color);
if (this.source === LabelsColorMapSource.Dashboard) {
this.colorMap.set(label, color);
}
}
/**
* Used to make sure all slices respect their original scheme.
*
* @param sliceId - the chart id
* @param ownScheme - the color scheme
*/
setOwnColorScheme(sliceId: number, ownScheme: string) {
const chartConfig = this.chartsLabelsMap.get(sliceId);
if (chartConfig) {
this.chartsLabelsMap.set(sliceId, {
...chartConfig,
ownScheme,
});
}
}
/**
* Remove a slice from the color map.
*
* @param sliceId - the chart
*/
removeSlice(sliceId: number) {
if (this.source !== LabelsColorMapSource.Dashboard) return;
@@ -96,10 +155,20 @@ export class LabelsColorMap {
this.colorMap = newColorMap;
}
/**
* Clear the shared labels color map.
*/
clear() {
this.chartsLabelsMap.clear();
this.colorMap.clear();
}
/**
* Clears all maps
*/
reset() {
this.clear();
this.chartsLabelsMap.clear();
}
}
const getInstance = makeSingleton(LabelsColorMap);

View File

@@ -161,4 +161,63 @@ describe('CategoricalColorNamespace', () => {
expect(color).toBe(color2);
});
});
describe('statis resetColorsForLabels(labels)', () => {
it('removes specified labels from forcedItems', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.setColor('label2', 'blue');
namespace.resetColorsForLabels(['label1']);
expect(namespace.forcedItems).toMatchObject({ label2: 'blue' });
});
it('does not modify forcedItems if no labels are provided', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.setColor('label2', 'blue');
namespace.resetColorsForLabels();
expect(namespace.forcedItems).toMatchObject({
label1: 'red',
label2: 'blue',
});
});
it('does nothing if the label is not in forcedItems', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.resetColorsForLabels(['label2']); // label2 doesn't exist
expect(namespace.forcedItems).toMatchObject({ label1: 'red' });
});
it('removes all labels when all are provided', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.setColor('label2', 'blue');
namespace.resetColorsForLabels(['label1', 'label2']);
expect(namespace.forcedItems).toMatchObject({});
});
it('creates a deep copy of forcedItems before modifying', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
const originalForcedItems = namespace.forcedItems;
namespace.resetColorsForLabels(['label1']);
expect(originalForcedItems).not.toBe(namespace.forcedItems);
});
it('removes the label if it exists in updatedForcedItems', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.resetColorsForLabels(['label1']);
expect(namespace.forcedItems).toEqual({});
});
it('does nothing for a label not in updatedForcedItems', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.resetColorsForLabels(['label2']); // label2 doesn't exist
expect(namespace.forcedItems).toEqual({ label1: 'red' });
});
});
});

View File

@@ -18,7 +18,11 @@
*/
import { ScaleOrdinal } from 'd3-scale';
import { CategoricalColorScale, FeatureFlag } from '@superset-ui/core';
import {
CategoricalColorScale,
FeatureFlag,
LabelsColorMapSource,
} from '@superset-ui/core';
describe('CategoricalColorScale', () => {
beforeEach(() => {
@@ -43,7 +47,6 @@ describe('CategoricalColorScale', () => {
expect(scale).toBeInstanceOf(CategoricalColorScale);
expect(scale.forcedColors).toBe(forcedColors);
});
it('can refer to colors based on their index', () => {
const forcedColors = { pig: 1, horse: 5 };
const scale = new CategoricalColorScale(
@@ -67,7 +70,7 @@ describe('CategoricalColorScale', () => {
>;
let getNextAvailableColorSpy: jest.SpyInstance<
string,
[currentColor: string]
[currentLabel: string, currentColor: string]
>;
beforeEach(() => {
@@ -83,6 +86,36 @@ describe('CategoricalColorScale', () => {
jest.restoreAllMocks();
});
it('uses labelsColorMapInstance color map when source is Dashboard, otherwise uses chartLabelsColorMap', () => {
const sliceId = 123;
const colorScheme = 'preset';
// Mock chartLabelsColorMap and labelsColorMapInstance's getColorMap
const chartColorMap = new Map([['testValueChart', 'chartColor']]);
const dashboardColorMap = new Map([['testValueDash', 'dashboardColor']]);
scale.chartLabelsColorMap = chartColorMap;
jest
.spyOn(scale.labelsColorMapInstance, 'getColorMap')
.mockReturnValue(dashboardColorMap);
// Test when source is Dashboard
scale.labelsColorMapInstance.source = LabelsColorMapSource.Dashboard;
const colorFromDashboard = scale.getColor(
'testValueDash',
sliceId,
colorScheme,
);
expect(colorFromDashboard).toBe('dashboardColor');
// Test when source is not Dashboard
scale.labelsColorMapInstance.source = LabelsColorMapSource.Explore;
const colorFromChart = scale.getColor(
'testValueChart',
sliceId,
colorScheme,
);
expect(colorFromChart).toBe('chartColor');
});
it('returns same color for same value', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green'], {
pig: 'red',
@@ -177,7 +210,10 @@ describe('CategoricalColorScale', () => {
scale.getColor('testValue3');
scale.getColor('testValue4');
expect(getNextAvailableColorSpy).toHaveBeenCalledWith('blue');
expect(getNextAvailableColorSpy).toHaveBeenCalledWith(
'testValue4',
'blue',
);
getNextAvailableColorSpy.mockClear();
@@ -289,23 +325,25 @@ describe('CategoricalColorScale', () => {
});
});
describe('.getNextAvailableColor(currentColor)', () => {
describe('.getNextAvailableColor(currentLabel, currentColor)', () => {
it('returns the current color if it is the least used or equally used among colors', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat');
scale.getColor('dog');
// Since 'green' hasn't been used, it's considered the least used.
expect(scale.getNextAvailableColor('blue')).toBe('green');
expect(scale.getNextAvailableColor('fish', 'blue')).toBe('green');
});
it('handles cases where all colors are equally used and returns the current color', () => {
it('returns the least used color among all', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
scale.getColor('fish'); // green
// All colors used once, so the function should return the current color
expect(scale.getNextAvailableColor('red')).toBe('red');
scale.getColor('puppy'); // blue
scale.getColor('teddy'); // red
// All colors used, so the function should return least used
expect(scale.getNextAvailableColor('darling', 'red')).toBe('green');
});
it('returns the least used color accurately even when some colors are used more frequently', () => {
@@ -324,7 +362,57 @@ describe('CategoricalColorScale', () => {
scale.getColor('pony'); // green
// Yellow is the least used color, so it should be returned.
expect(scale.getNextAvailableColor('blue')).toBe('yellow');
expect(scale.getNextAvailableColor('pony', 'blue')).toBe('yellow');
});
it('does not return adjacent colors if a non-adjacent color is equally used', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.chartLabelsColorMap.set('label1', 'red'); // Adjacent
scale.chartLabelsColorMap.set('label2', 'blue'); // currentLabel
scale.chartLabelsColorMap.set('label3', 'green'); // Adjacent
// Green and blue are equally used, but green is adjacent and penalized.
expect(scale.getNextAvailableColor('label2', 'blue')).toBe('blue');
});
it('prioritizes a color that has never been used, even if there are adjacent colors', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
scale.chartLabelsColorMap.set('label1', 'red');
scale.chartLabelsColorMap.set('label2', 'blue'); // currentLabel
// Green has never been used, so it is prioritized.
expect(scale.getNextAvailableColor('label2', 'blue')).toBe('green');
});
it('returns the least used or unused color when there are no adjacent labels', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
// No adjacent labels are defined in chartLabelsColorMap.
expect(scale.getNextAvailableColor('label2', 'green')).toBe('green');
});
it('handles colors that have never been used (fallback to usage count 0)', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
// Do not use "green" at all
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
// "green" has never been used, so usageCount for "green" should fallback to 0
expect(scale.getNextAvailableColor('label2', 'red')).toBe('green');
});
it('handles a color with an explicit usage count of 0', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
// Mock or override getColorUsageCount to return 0 for "blue"
jest.spyOn(scale, 'getColorUsageCount').mockImplementation(color => {
if (color === 'blue') return 0; // Explicitly return 0 for "blue"
return 1; // Return 1 for other colors
});
// "blue" should still be a valid option with a usage count of 0
expect(scale.getNextAvailableColor('label1', 'red')).toBe('blue');
});
});

View File

@@ -53,7 +53,7 @@ describe('LabelsColorMap', () => {
beforeEach(() => {
getLabelsColorMap().source = LabelsColorMapSource.Dashboard;
getLabelsColorMap().clear();
getLabelsColorMap().reset();
});
it('has default value out-of-the-box', () => {
@@ -92,11 +92,17 @@ describe('LabelsColorMap', () => {
expect(Object.fromEntries(colorMap)).toEqual({ b: 'green' });
});
it('should do nothing when source is not dashboard', () => {
it('should set a new color only when source is dashboard', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Explore;
labelsColorMap.addSlice('a', 'red', 1);
expect(Object.fromEntries(labelsColorMap.chartsLabelsMap)).toEqual({});
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({});
labelsColorMap.source = LabelsColorMapSource.Dashboard;
labelsColorMap.addSlice('a', 'red', 1);
const colorMap2 = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap2)).toEqual({ a: 'red' });
});
});
@@ -126,7 +132,7 @@ describe('LabelsColorMap', () => {
});
});
describe('.updateColorMap(namespace, scheme)', () => {
describe('.updateColorMap(namespace, scheme, merge)', () => {
let categoricalNamespace: any;
let mockedNamespace: any;
let labelsColorMap: any;
@@ -141,18 +147,24 @@ describe('LabelsColorMap', () => {
};
});
it('should clear color map when not merge', () => {
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2', false);
expect(labelsColorMap.colorMap).toEqual(new Map([['a', 'mockColor']]));
});
it('should not clear color map when merge', () => {
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2', true);
expect(labelsColorMap.colorMap).not.toEqual(new Map());
});
it('should use provided color scheme', () => {
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2');
expect(mockedNamespace.getScale).toHaveBeenCalledWith('testColors2');
});
it('should fallback to original chart color scheme if no color scheme is provided', () => {
labelsColorMap.addSlice('a', 'red', 1, 'originalScheme');
labelsColorMap.updateColorMap(mockedNamespace);
expect(mockedNamespace.getScale).toHaveBeenCalledWith('originalScheme');
});
it('should fallback to undefined if no color scheme is provided', () => {
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.addSlice('b', 'blue', 2);
@@ -181,6 +193,23 @@ describe('LabelsColorMap', () => {
});
});
it('should update only new labels in the color map when merge', () => {
labelsColorMap.colorMap = new Map();
labelsColorMap.addSlice('a', 'yellow', 1);
labelsColorMap.addSlice('b', 'green', 1);
labelsColorMap.addSlice('c', 'purple', 1);
labelsColorMap.updateColorMap(categoricalNamespace, 'testColors2', true);
const mergedColorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(mergedColorMap)).toEqual({
a: 'yellow',
b: 'green',
c: 'purple',
});
});
it('should use recycle colors', () => {
window.featureFlags = {
[FeatureFlag.UseAnalagousColors]: false,
@@ -231,4 +260,47 @@ describe('LabelsColorMap', () => {
expect(Object.fromEntries(colorMap)).toEqual({});
});
});
describe('setOwnColorScheme(sliceId, ownScheme)', () => {
it('should update the scheme in the config', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Explore;
const sliceId = 1;
const initialConfig = { labels: ['initial config'] };
labelsColorMap.chartsLabelsMap = new Map();
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
...initialConfig,
ownScheme: 'newScheme',
});
});
it('should update ownScheme when source is not Explore', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Dashboard;
const sliceId = 1;
const initialConfig = { labels: ['initial config'] };
labelsColorMap.chartsLabelsMap = new Map();
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
...initialConfig,
ownScheme: 'newScheme',
});
});
it('should do nothing when chart config does not exist', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Explore;
const sliceId = 1;
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual(undefined);
});
});
});

View File

@@ -1,13 +0,0 @@
{
"name": "@superset-ui/switchboard",
"version": "0.18.26-0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@superset-ui/switchboard",
"version": "0.18.26-0",
"license": "Apache-2.0"
}
}
}

View File

@@ -93,7 +93,7 @@ function Chord(element, props) {
.append('path')
.attr('id', (d, i) => `group${i}`)
.attr('d', arc)
.style('fill', (d, i) => colorFn(nodes[i], sliceId, colorScheme));
.style('fill', (d, i) => colorFn(nodes[i], sliceId));
// Add a text label.
const groupText = group.append('text').attr('x', 6).attr('dy', 15);
@@ -121,7 +121,7 @@ function Chord(element, props) {
.on('mouseover', d => {
chord.classed('fade', p => p !== d);
})
.style('fill', d => colorFn(nodes[d.source.index], sliceId, colorScheme))
.style('fill', d => colorFn(nodes[d.source.index], sliceId))
.attr('d', path);
// Add an elaborate mouseover title for each chord.

View File

@@ -37,6 +37,7 @@ const propTypes = {
width: PropTypes.number,
height: PropTypes.number,
country: PropTypes.string,
colorScheme: PropTypes.string,
linearColorScheme: PropTypes.string,
mapBaseUrl: PropTypes.string,
numberFormat: PropTypes.string,

View File

@@ -73,12 +73,11 @@ class CustomHistogram extends PureComponent {
showLegend,
sliceId,
} = this.props;
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
const keys = data.map(d => d.key);
const colorScale = scaleOrdinal({
domain: keys,
range: keys.map(x => colorFn(x, sliceId, colorScheme)),
range: keys.map(x => colorFn(x, sliceId)),
});
return (

View File

@@ -384,7 +384,7 @@ function Icicle(element, props) {
// Apply color scheme
g.selectAll('rect').style('fill', d => {
d.color = colorFn(d.name, sliceId, colorScheme);
d.color = colorFn(d.name, sliceId);
return d.color;
});

View File

@@ -46,6 +46,7 @@ const propTypes = {
numberFormat: PropTypes.string,
useRichTooltip: PropTypes.bool,
useAreaProportions: PropTypes.bool,
colorScheme: PropTypes.string,
};
function copyArc(d) {
@@ -120,14 +121,14 @@ function Rose(element, props) {
.map(v => ({
key: v.name,
value: v.value,
color: colorFn(v.name, sliceId, colorScheme),
color: colorFn(v.name, sliceId),
highlight: v.id === d.arcId,
}))
: [
{
key: d.name,
value: d.val,
color: colorFn(d.name, sliceId, colorScheme),
color: colorFn(d.name, sliceId),
},
];
@@ -138,7 +139,7 @@ function Rose(element, props) {
};
}
legend.width(width).color(d => colorFn(d.key, sliceId, colorScheme));
legend.width(width).color(d => colorFn(d.key, sliceId));
legendWrap.datum(legendData(datum)).call(legend);
tooltip.headerFormatter(timeFormat).valueFormatter(format);
@@ -385,7 +386,7 @@ function Rose(element, props) {
const arcs = ae
.append('path')
.attr('class', 'arc')
.attr('fill', d => colorFn(d.name, sliceId, colorScheme))
.attr('fill', d => colorFn(d.name, sliceId))
.attr('d', arc);
function mousemove() {

View File

@@ -83,7 +83,7 @@ function computeGraph(links) {
function SankeyLoop(element, props) {
const { data, width, height, colorScheme, sliceId } = props;
const color = CategoricalColorNamespace.getScale(colorScheme);
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
const margin = { ...defaultMargin, ...props.margin };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
@@ -107,7 +107,7 @@ function SankeyLoop(element, props) {
value / sValue,
)})`,
)
.linkColor(d => color(d.source.name, sliceId));
.linkColor(d => colorFn(d.source.name, sliceId));
const div = select(element);
div.selectAll('*').remove();

View File

@@ -67,7 +67,6 @@ function Sankey(element, props) {
.attr('height', innerHeight + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
const sankey = d3Sankey()
@@ -219,7 +218,7 @@ function Sankey(element, props) {
.attr('width', sankey.nodeWidth())
.style('fill', d => {
const name = d.name || 'N/A';
d.color = colorFn(name, sliceId, colorScheme);
d.color = colorFn(name, sliceId);
return d.color;
})

View File

@@ -43,6 +43,7 @@ const propTypes = {
showBubbles: PropTypes.bool,
linearColorScheme: PropTypes.string,
color: PropTypes.string,
colorScheme: PropTypes.string,
setDataMask: PropTypes.func,
onContextMenu: PropTypes.func,
emitCrossFilters: PropTypes.bool,
@@ -85,24 +86,24 @@ function WorldMap(element, props) {
.range([1, maxBubbleSize]);
let processedData;
let colorScale;
let colorFn;
if (colorBy === ColorBy.Country) {
colorScale = CategoricalColorNamespace.getScale(colorScheme);
colorFn = CategoricalColorNamespace.getScale(colorScheme);
processedData = filteredData.map(d => ({
...d,
radius: radiusScale(Math.sqrt(d.m2)),
fillColor: colorScale(d.name, sliceId),
fillColor: colorFn(d.name, sliceId),
}));
} else {
colorScale = getSequentialSchemeRegistry()
colorFn = getSequentialSchemeRegistry()
.get(linearColorScheme)
.createLinearScale(d3Extent(filteredData, d => d.m1));
processedData = filteredData.map(d => ({
...d,
radius: radiusScale(Math.sqrt(d.m2)),
fillColor: colorScale(d.m1),
fillColor: colorFn(d.m1),
}));
}

View File

@@ -52,16 +52,14 @@ const { getScale } = CategoricalColorNamespace;
function getCategories(fd: QueryFormData, data: JsonObject[]) {
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
const colorFn = getScale(fd.color_scheme);
const appliedScheme = fd.color_scheme;
const colorFn = getScale(appliedScheme);
const categories = {};
data.forEach(d => {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
let color;
if (fd.dimension) {
color = hexToRGB(
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
c.a * 255,
);
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
} else {
color = fixedColor;
}
@@ -132,15 +130,13 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
const addColor = useCallback((data: JsonObject[], fd: QueryFormData) => {
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const colorFn = getScale(fd.color_scheme);
const appliedScheme = fd.color_scheme;
const colorFn = getScale(appliedScheme);
return data.map(d => {
let color;
if (fd.dimension) {
color = hexToRGB(
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
c.a * 255,
);
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
return { ...d, color };
}

View File

@@ -55,7 +55,8 @@ export function getLayer(
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
) {
const fd = formData;
const colorScale = CategoricalColorNamespace.getScale(fd.color_scheme);
const appliedScheme = fd.color_scheme;
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
const colorRange = colorScale
.range()
.map(color => hexToRGB(color)) as Color[];

View File

@@ -54,7 +54,8 @@ export function getLayer(
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
) {
const fd = formData;
const colorScale = CategoricalColorNamespace.getScale(fd.color_scheme);
const appliedScheme = fd.color_scheme;
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
const colorRange = colorScale
.range()
.map(color => hexToRGB(color)) as Color[];

View File

@@ -658,9 +658,7 @@ function nvd3Vis(element, props) {
} else if (vizType !== 'bullet') {
const colorFn = getScale(colorScheme);
chart.color(
d =>
d.color ||
colorFn(cleanColorInput(d[colorKey]), sliceId, colorScheme),
d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId),
);
}

View File

@@ -109,9 +109,9 @@ export default function transformProps(
datum[`${metric}__outliers`],
],
itemStyle: {
color: colorFn(groupbyLabel, sliceId, colorScheme),
color: colorFn(groupbyLabel, sliceId),
opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6,
borderColor: colorFn(groupbyLabel, sliceId, colorScheme),
borderColor: colorFn(groupbyLabel, sliceId),
},
};
});
@@ -150,7 +150,7 @@ export default function transformProps(
},
},
itemStyle: {
color: colorFn(groupbyLabel, sliceId, colorScheme),
color: colorFn(groupbyLabel, sliceId),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,

View File

@@ -108,8 +108,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
legendOrientation,
legendMargin,
legendType,
sliceId,
}: EchartsBubbleFormData = { ...DEFAULT_FORM_DATA, ...formData };
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const legends = new Set<string>();
@@ -138,7 +138,10 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
],
],
type: 'scatter',
itemStyle: { color: colorFn(name), opacity },
itemStyle: {
color: colorFn(name, sliceId),
opacity,
},
});
legends.add(name);
});

View File

@@ -145,7 +145,6 @@ export default function transformProps(
}, {});
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getValueFormatter(
metric,
@@ -175,7 +174,7 @@ export default function transformProps(
value,
name,
itemStyle: {
color: colorFn(name, sliceId, colorScheme),
color: colorFn(name, sliceId),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,

View File

@@ -166,6 +166,7 @@ export default function transformProps(
const name = groupbyLabels
.map(column => `${verboseMap[column] || column}: ${data_point[column]}`)
.join(', ');
const colorLabel = groupbyLabels.map(col => data_point[col] as string);
columnsLabelMap.set(
name,
groupbyLabels.map(col => data_point[col] as string),
@@ -174,7 +175,7 @@ export default function transformProps(
value: data_point[metricLabel] as number,
name,
itemStyle: {
color: colorFn(index, sliceId, colorScheme),
color: colorFn(colorLabel, sliceId),
},
title: {
offsetCenter: [
@@ -202,7 +203,7 @@ export default function transformProps(
item = {
...item,
itemStyle: {
color: colorFn(index, sliceId, colorScheme),
color: colorFn(index, sliceId),
opacity: OpacityEnum.SemiTransparent,
},
detail: {

View File

@@ -297,14 +297,15 @@ export default function transformProps(
});
const categoryList = [...categories];
const series: GraphSeriesOption[] = [
{
zoom: DEFAULT_GRAPH_SERIES_OPTION.zoom,
type: 'graph',
categories: categoryList.map(c => ({
name: c,
itemStyle: { color: colorFn(c, sliceId, colorScheme) },
itemStyle: {
color: colorFn(c, sliceId),
},
})),
layout,
force: {

View File

@@ -191,7 +191,6 @@ export default function transformProps(
}, {});
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getValueFormatter(
metric,
@@ -223,7 +222,7 @@ export default function transformProps(
value,
name,
itemStyle: {
color: colorFn(name, sliceId, colorScheme),
color: colorFn(name, sliceId),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,

View File

@@ -109,7 +109,6 @@ export default function transformProps(
...formData,
};
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getNumberFormatter(numberFormat);
const formatter = (params: CallbackDataParams) =>
@@ -182,7 +181,7 @@ export default function transformProps(
value: metricLabels.map(metricLabel => datum[metricLabel]),
name: joinedName,
itemStyle: {
color: colorFn(joinedName, sliceId, colorScheme),
color: colorFn(joinedName, sliceId),
opacity: isFiltered
? OpacityEnum.Transparent
: OpacityEnum.NonTransparent,

View File

@@ -41,7 +41,7 @@ export default function transformProps(
const refs: Refs = {};
const { formData, height, hooks, queriesData, width } = chartProps;
const { onLegendStateChanged } = hooks;
const { colorScheme, metric, source, target } = formData;
const { colorScheme, metric, source, target, sliceId } = formData;
const { data } = queriesData[0];
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
const metricLabel = getMetricLabel(metric);
@@ -68,7 +68,7 @@ export default function transformProps(
).map(name => ({
name,
itemStyle: {
color: colorFn(name),
color: colorFn(name, sliceId),
},
}));

View File

@@ -202,7 +202,6 @@ export default function transformProps(
}
return { ...acc, [entry[0]]: entry[1] };
}, {});
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
const rebasedData = rebaseForecastDatum(data, verboseMap);
let xAxisLabel = getXAxisLabel(chartProps.rawFormData) as string;

View File

@@ -176,18 +176,18 @@ export default function transformProps(
let item: TreemapSeriesNodeItemOption = {
name,
value,
colorSaturation: COLOR_SATURATION,
itemStyle: {
borderColor: BORDER_COLOR,
color: colorFn(name, sliceId),
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
},
};
if (treeNode.children?.length) {
item = {
...item,
children: traverse(treeNode.children, newPath),
colorSaturation: COLOR_SATURATION,
itemStyle: {
borderColor: BORDER_COLOR,
color: colorFn(name, sliceId, colorScheme),
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
},
};
} else {
const joinedName = newPath.join(',');
@@ -217,7 +217,7 @@ export default function transformProps(
colorSaturation: COLOR_SATURATION,
itemStyle: {
borderColor: BORDER_COLOR,
color: colorFn(`${metricLabel}`, sliceId, colorScheme),
color: colorFn(`${metricLabel}`, sliceId),
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
},

View File

@@ -29,7 +29,7 @@ import {
SupersetThemeProps,
withTheme,
seed,
CategoricalColorScale,
CategoricalColorNamespace,
} from '@superset-ui/core';
import { isEqual } from 'lodash';
@@ -230,7 +230,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
encoder.channels.color.setDomainFromDataset(words);
const { getValueFromDatum } = encoder.channels.color;
const colorFn = encoder.channels.color.scale as CategoricalColorScale;
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
const viewBoxWidth = width * scaleFactor;
const viewBoxHeight = height * scaleFactor;
@@ -250,11 +250,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
fontSize={`${w.size}px`}
fontWeight={w.weight}
fontFamily={w.font}
fill={colorFn(
getValueFromDatum(w) as string,
sliceId,
colorScheme,
)}
fill={colorFn(getValueFromDatum(w) as string, sliceId)}
textAnchor="middle"
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
>

View File

@@ -26,10 +26,7 @@ export const getDashboardFormData = (overrides: JsonObject = {}) => ({
girl: '#FF69B4',
boy: '#ADD8E6',
},
shared_label_colors: {
boy: '#ADD8E6',
girl: '#FF69B4',
},
shared_label_colors: ['boy', 'girl'],
color_scheme: 'd3Category20b',
extra_filters: [
{

View File

@@ -113,6 +113,6 @@ export const overwriteConfirmMetadata = {
slug: null,
owners: [],
json_metadata:
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":{"Male":"#5ac19e","Female":"#1f86c9","<NULL>":"#5AC189","Prefer not to say":"#47457c","No Answer":"#e05043","Yes, an ethnic minority":"#666666","No, not an ethnic minority":"#ffa444","age":"#1FA8C9"},"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":["Male", "Female","<NULL>","Prefer not to say","No Answer","Yes, an ethnic minority","No, not an ethnic minority","age"],"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
},
};

View File

@@ -51,7 +51,7 @@ export type LastModified = {
export type Owner = {
type: MetadataType.Owner;
createdBy: string;
owners?: string[];
owners?: string[] | string;
createdOn: string;
onClick?: (type: string) => void;
};

View File

@@ -23,27 +23,18 @@ import { styled } from '@superset-ui/core';
import { Tooltip, TooltipPlacement } from 'src/components/Tooltip';
import { ContentType } from './ContentType';
import { config } from './ContentConfig';
export const MIN_NUMBER_ITEMS = 2;
export const MAX_NUMBER_ITEMS = 6;
const HORIZONTAL_PADDING = 12;
const VERTICAL_PADDING = 8;
const ICON_PADDING = 8;
const SPACE_BETWEEN_ITEMS = 16;
const ICON_WIDTH = 16;
const TEXT_MIN_WIDTH = 70;
const TEXT_MAX_WIDTH = 150;
const ORDER = {
dashboards: 0,
table: 1,
sql: 2,
rows: 3,
tags: 4,
description: 5,
owner: 6,
lastModified: 7,
};
import {
HORIZONTAL_PADDING,
ICON_PADDING,
ICON_WIDTH,
VERTICAL_PADDING,
TEXT_MIN_WIDTH,
TEXT_MAX_WIDTH,
SPACE_BETWEEN_ITEMS,
ORDER,
MIN_NUMBER_ITEMS,
MAX_NUMBER_ITEMS,
} from './constants';
const Bar = styled.div<{ count: number }>`
${({ theme, count }) => `

View File

@@ -0,0 +1,39 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const MIN_NUMBER_ITEMS = 2;
export const MAX_NUMBER_ITEMS = 6;
export const HORIZONTAL_PADDING = 12;
export const VERTICAL_PADDING = 8;
export const ICON_PADDING = 8;
export const SPACE_BETWEEN_ITEMS = 16;
export const ICON_WIDTH = 16;
export const TEXT_MIN_WIDTH = 70;
export const TEXT_MAX_WIDTH = 150;
export const ORDER = {
dashboards: 0,
table: 1,
sql: 2,
rows: 3,
tags: 4,
description: 5,
owner: 6,
lastModified: 7,
};

View File

@@ -16,7 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import MetadataBar, { MIN_NUMBER_ITEMS, MAX_NUMBER_ITEMS } from './MetadataBar';
import MetadataBar from './MetadataBar';
import { MIN_NUMBER_ITEMS, MAX_NUMBER_ITEMS } from './constants';
export type { MetadataBarProps } from './MetadataBar';

View File

@@ -55,6 +55,7 @@ import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import { safeStringify } from 'src/utils/safeStringify';
import { logEvent } from 'src/logger/actions';
import { LOG_ACTIONS_CONFIRM_OVERWRITE_DASHBOARD_METADATA } from 'src/logger/LogUtils';
import { isEqual } from 'lodash';
import { UPDATE_COMPONENTS_PARENTS_LIST } from './dashboardLayout';
import {
saveChartConfiguration,
@@ -68,9 +69,10 @@ import getOverwriteItems from '../util/getOverwriteItems';
import {
applyColors,
isLabelsColorMapSynced,
getLabelsColorMapEntries,
getColorSchemeDomain,
getColorNamespace,
getLabelsColorMapEntries,
getFreshSharedLabels,
} from '../../utils/colorScheme';
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
@@ -224,6 +226,41 @@ export function saveDashboardFinished() {
return { type: SAVE_DASHBOARD_FINISHED };
}
export const SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE =
'SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE';
export const SET_DASHBOARD_LABELS_COLORMAP_SYNCED =
'SET_DASHBOARD_LABELS_COLORMAP_SYNCED';
export const SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE =
'SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE';
export const SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED =
'SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED';
export function setDashboardLabelsColorMapSync() {
return { type: SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE };
}
export function setDashboardLabelsColorMapSynced() {
return { type: SET_DASHBOARD_LABELS_COLORMAP_SYNCED };
}
export function setDashboardSharedLabelsColorsSync() {
return { type: SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE };
}
export function setDashboardSharedLabelsColorsSynced() {
return { type: SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED };
}
export const setDashboardMetadata = updatedMetadata => async dispatch => {
dispatch(
dashboardInfoChanged({
metadata: {
...updatedMetadata,
},
}),
);
};
export function saveDashboardRequest(data, id, saveType) {
return (dispatch, getState) => {
dispatch({ type: UPDATE_COMPONENTS_PARENTS_LIST });
@@ -254,6 +291,9 @@ export function saveDashboardRequest(data, id, saveType) {
const hasId = item => item.id !== undefined;
const metadataCrossFiltersEnabled = data.metadata?.cross_filters_enabled;
const colorScheme = data.metadata?.color_scheme;
const customLabelsColor = data.metadata?.label_colors || {};
const sharedLabelsColor = data.metadata?.shared_label_colors || [];
// making sure the data is what the backend expects
const cleanedData = {
...data,
@@ -270,11 +310,14 @@ export function saveDashboardRequest(data, id, saveType) {
metadata: {
...data.metadata,
color_namespace: getColorNamespace(data.metadata?.color_namespace),
color_scheme: data.metadata?.color_scheme || '',
color_scheme_domain: data.metadata?.color_scheme_domain || [],
color_scheme: colorScheme || '',
color_scheme_domain: colorScheme
? getColorSchemeDomain(colorScheme)
: [],
expanded_slices: data.metadata?.expanded_slices || {},
label_colors: data.metadata?.label_colors || {},
shared_label_colors: data.metadata?.shared_label_colors || {},
label_colors: customLabelsColor,
shared_label_colors: getFreshSharedLabels(sharedLabelsColor),
map_label_colors: getLabelsColorMapEntries(customLabelsColor),
refresh_frequency: data.metadata?.refresh_frequency || 0,
timed_refresh_immune_slices:
data.metadata?.timed_refresh_immune_slices || [],
@@ -324,11 +367,7 @@ export function saveDashboardRequest(data, id, saveType) {
// syncing with the backend transformations of the metadata
if (updatedDashboard.json_metadata) {
const metadata = JSON.parse(updatedDashboard.json_metadata);
dispatch(
dashboardInfoChanged({
metadata,
}),
);
dispatch(setDashboardMetadata(metadata));
if (metadata.chart_configuration) {
dispatch({
type: SAVE_CHART_CONFIG_COMPLETE,
@@ -677,68 +716,261 @@ export function setDatasetsStatus(status) {
};
}
const updateDashboardMetadata = async (id, metadata, dispatch) => {
await SupersetClient.put({
const storeDashboardMetadata = async (id, metadata) =>
SupersetClient.put({
endpoint: `/api/v1/dashboard/${id}`,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ json_metadata: JSON.stringify(metadata) }),
});
dispatch(dashboardInfoChanged({ metadata }));
};
export const updateDashboardLabelsColor = () => async (dispatch, getState) => {
/**
*
* Persists the label colors maps in the dashboard metadata.
* It runs when outdated color info are detected in stored metadata.
*
* @returns void
*/
export const persistDashboardLabelsColor = () => async (dispatch, getState) => {
const {
dashboardInfo: { id, metadata },
dashboardState: { labelsColorMapMustSync, sharedLabelsColorsMustSync },
} = getState();
const categoricalSchemes = getCategoricalSchemeRegistry();
const colorScheme = metadata?.color_scheme;
const colorSchemeRegistry = categoricalSchemes.get(
metadata?.color_scheme,
true,
);
const defaultScheme = categoricalSchemes.defaultKey;
const fallbackScheme = defaultScheme?.toString() || 'supersetColors';
const colorSchemeDomain = metadata?.color_scheme_domain || [];
if (labelsColorMapMustSync || sharedLabelsColorsMustSync) {
storeDashboardMetadata(id, metadata);
dispatch(setDashboardLabelsColorMapSynced());
dispatch(setDashboardSharedLabelsColorsSynced());
}
};
/**
* Checks the stored dashboard metadata for inconsistencies.
* Update the current metadata with validated color information.
* It runs only on Dashboard page load.
*
* @param {*} metadata - the stored dashboard metadata
* @returns void
*/
export const applyDashboardLabelsColorOnLoad = metadata => async dispatch => {
try {
const updatedMetadata = { ...metadata };
let updatedScheme = metadata?.color_scheme;
const customLabelsColor = metadata.label_colors || {};
const sharedLabelsColor = metadata.shared_label_colors || [];
let hasChanged = false;
// backward compatibility of shared_label_colors
const sharedLabels = metadata.shared_label_colors || [];
if (!Array.isArray(sharedLabels) && Object.keys(sharedLabels).length > 0) {
hasChanged = true;
updatedMetadata.shared_label_colors = getFreshSharedLabels(
Object.keys(sharedLabelsColor),
);
}
// backward compatibility of map_label_colors
const hasMapLabelColors =
Object.keys(metadata.map_label_colors || {}).length > 0;
let updatedScheme = metadata.color_scheme;
const categoricalSchemes = getCategoricalSchemeRegistry();
const colorSchemeRegistry = categoricalSchemes.get(updatedScheme, true);
const hasInvalidColorScheme = !!updatedScheme && !colorSchemeRegistry;
// color scheme might not exist any longer
if (hasInvalidColorScheme) {
const defaultScheme = categoricalSchemes.defaultKey;
const fallbackScheme = defaultScheme?.toString() || 'supersetColors';
hasChanged = true;
// Color scheme does not exist anymore, fallback to default
if (colorScheme && !colorSchemeRegistry) {
updatedScheme = fallbackScheme;
updatedMetadata.color_scheme = updatedScheme;
updatedMetadata.color_scheme_domain = getColorSchemeDomain(colorScheme);
dispatch(setColorScheme(updatedScheme));
// must re-apply colors from fresh labels color map
applyColors(updatedMetadata, true);
}
// stored labels color map and applied might differ
const isMapSynced = isLabelsColorMapSynced(metadata);
if (!isMapSynced) {
// re-apply a fresh labels color map
applyColors(updatedMetadata, true);
// pull and store the just applied labels color map
updatedMetadata.shared_label_colors = getLabelsColorMapEntries();
}
// the stored color domain registry and fresh might differ at this point
const freshColorSchemeDomain = getColorSchemeDomain(colorScheme);
const isRegistrySynced =
colorSchemeDomain.toString() !== freshColorSchemeDomain.toString();
if (colorScheme && !isRegistrySynced) {
const freshColorSchemeDomain = updatedScheme
? getColorSchemeDomain(updatedScheme)
: [];
const currentColorSchemeDomain = metadata.color_scheme_domain || [];
if (!isEqual(freshColorSchemeDomain, currentColorSchemeDomain)) {
hasChanged = true;
updatedMetadata.color_scheme_domain = freshColorSchemeDomain;
}
if (
(colorScheme && (!colorSchemeRegistry || !isRegistrySynced)) ||
!isMapSynced
) {
await updateDashboardMetadata(id, updatedMetadata, dispatch);
// if color scheme is invalid or map is missing, apply a fresh color map
// if valid, apply the stored map to keep consistency across refreshes
const shouldGoFresh = !hasMapLabelColors || hasInvalidColorScheme;
applyColors(updatedMetadata, shouldGoFresh);
if (shouldGoFresh) {
// a fresh color map has been applied
// needs to be stored for consistency
hasChanged = true;
updatedMetadata.map_label_colors =
getLabelsColorMapEntries(customLabelsColor);
}
} catch (error) {
console.error('Failed to update dashboard color settings:', error);
if (hasChanged) {
dispatch(setDashboardMetadata(updatedMetadata));
dispatch(setDashboardLabelsColorMapSync());
}
} catch (e) {
console.error('Failed to update dashboard color on load:', e);
}
};
/**
*
* Ensure that the stored color map matches fresh map.
*
* @param {*} metadata - the dashboard metadata
* @returns void
*/
export const ensureSyncedLabelsColorMap = metadata => (dispatch, getState) => {
const {
dashboardState: { labelsColorMapMustSync },
} = getState();
const updatedMetadata = { ...metadata };
const customLabelsColor = metadata.label_colors || {};
const isMapSynced = isLabelsColorMapSynced(metadata);
const mustSync = !isMapSynced;
if (mustSync) {
const freshestColorMapEntries = getLabelsColorMapEntries(customLabelsColor);
updatedMetadata.map_label_colors = freshestColorMapEntries;
dispatch(setDashboardMetadata(updatedMetadata));
}
if (mustSync && !labelsColorMapMustSync) {
// prepare to persist the just applied labels color map
dispatch(setDashboardLabelsColorMapSync());
}
if (!mustSync && labelsColorMapMustSync) {
dispatch(setDashboardLabelsColorMapSynced());
}
};
/**
*
* Ensure that the stored shared labels colors match current.
*
* @param {*} metadata - the dashboard metadata
* @returns void
*/
export const ensureSyncedSharedLabelsColors =
metadata => (dispatch, getState) => {
// using a timeout to let the rendered charts finish processing labels
setTimeout(() => {
const {
dashboardState: { sharedLabelsColorsMustSync },
} = getState();
const updatedMetadata = { ...metadata };
const sharedLabelsColors = metadata.shared_label_colors || [];
const freshLabelsColors = getFreshSharedLabels(sharedLabelsColors);
const isSharedLabelsColorsSynced = isEqual(
sharedLabelsColors,
freshLabelsColors,
);
const mustSync = !isSharedLabelsColorsSynced;
if (mustSync) {
updatedMetadata.shared_label_colors = freshLabelsColors;
dispatch(setDashboardMetadata(updatedMetadata));
}
if (mustSync && !sharedLabelsColorsMustSync) {
// prepare to persist the shared labels colors
dispatch(setDashboardSharedLabelsColorsSync());
}
if (!mustSync && sharedLabelsColorsMustSync) {
dispatch(setDashboardSharedLabelsColorsSynced());
}
}, 500);
};
/**
*
* Updates the color map with new labels and colors as they appear.
*
* @param {*} renderedChartIds - the charts that have finished rendering
* @returns void
*/
export const updateDashboardLabelsColor =
renderedChartIds => (dispatch, getState) => {
try {
const {
dashboardInfo: { metadata },
charts,
} = getState();
const colorScheme = metadata.color_scheme;
const labelsColorMapInstance = getLabelsColorMap();
const fullLabelsColors = metadata.map_label_colors || {};
const sharedLabelsColors = metadata.shared_label_colors || [];
const customLabelsColors = metadata.label_colors || {};
const updatedMetadata = { ...metadata };
// for dashboards with no color scheme, the charts should always use their individual schemes
// this logic looks for unique labels (not shared across multiple charts) of each rendered chart
// it applies a new color to those unique labels when the applied scheme is not up to date
// while leaving shared label colors and custom label colors intact for color consistency
const shouldReset = [];
if (renderedChartIds.length > 0) {
const sharedLabelsSet = new Set(sharedLabelsColors);
renderedChartIds.forEach(id => {
const chart = charts[id];
const formData = chart.form_data || chart.latestQueryFormData;
// ensure charts have their original color scheme always available
labelsColorMapInstance.setOwnColorScheme(
formData.slice_id,
formData.color_scheme,
);
// if dashboard has a scheme, charts should ignore individual schemes
// thus following logic is inapplicable if a dashboard color scheme exists
if (colorScheme) return;
const chartColorScheme = formData.color_scheme;
const currentChartConfig = labelsColorMapInstance.chartsLabelsMap.get(
formData.slice_id,
);
const currentChartLabels = currentChartConfig?.labels || [];
const uniqueChartLabels = currentChartLabels.filter(
l =>
!sharedLabelsSet.has(l) && !customLabelsColors.hasOwnProperty(l),
);
// Map unique labels to colors
const uniqueChartLabelsColor = new Set(
uniqueChartLabels.map(l => fullLabelsColors[l]).filter(Boolean),
);
const expectedColorsForChartScheme = new Set(
getColorSchemeDomain(chartColorScheme),
);
// Check if any unique label color is not in the expected colors set
const shouldResetColors = [...uniqueChartLabelsColor].some(
color => !expectedColorsForChartScheme.has(color),
);
// Only push uniqueChartLabels if they require resetting
if (shouldResetColors) shouldReset.push(...uniqueChartLabels);
});
}
// an existing map is available, use mrge option
// to only apply colors to newly found labels
const shouldGoFresh = shouldReset.length > 0 ? shouldReset : false;
const shouldMerge = !shouldGoFresh;
// re-apply the color map first to get fresh maps accordingly
applyColors(updatedMetadata, shouldGoFresh, shouldMerge);
// new data may have appeared in the map (data changes)
// or new slices may have appeared while changing tabs
dispatch(ensureSyncedLabelsColorMap(updatedMetadata));
dispatch(ensureSyncedSharedLabelsColors(updatedMetadata));
} catch (e) {
console.error('Failed to update colors for new charts and labels:', e);
}
};

View File

@@ -36,6 +36,8 @@ import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
fetchMock.put('glob:*/api/v1/dashboard/*', {});
jest.mock('src/dashboard/actions/dashboardState', () => ({
...jest.requireActual('src/dashboard/actions/dashboardState'),
fetchFaveStar: jest.fn(),

View File

@@ -34,7 +34,7 @@ import { Global } from '@emotion/react';
import { useDispatch, useSelector } from 'react-redux';
import ErrorBoundary from 'src/components/ErrorBoundary';
import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane';
import DashboardHeader from 'src/dashboard/containers/DashboardHeader';
import DashboardHeader from 'src/dashboard/components/Header';
import Icons from 'src/components/Icons';
import IconButton from 'src/dashboard/components/IconButton';
import { Droppable } from 'src/dashboard/components/dnd/DragDroppable';

View File

@@ -18,7 +18,7 @@
*/
// ParentSize uses resize observer so the dashboard will update size
// when its container size changes, due to e.g., builder side panel opening
import { FC, useEffect, useMemo, useRef } from 'react';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
Filter,
@@ -43,12 +43,12 @@ import {
import { getChartIdsInFilterScope } from 'src/dashboard/util/getChartIdsInFilterScope';
import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId';
import { setInScopeStatusOfFilters } from 'src/dashboard/actions/nativeFilters';
import { updateDashboardLabelsColor } from 'src/dashboard/actions/dashboardState';
import {
applyColors,
getColorNamespace,
resetColors,
} from 'src/utils/colorScheme';
applyDashboardLabelsColorOnLoad,
updateDashboardLabelsColor,
persistDashboardLabelsColor,
} from 'src/dashboard/actions/dashboardState';
import { getColorNamespace, resetColors } from 'src/utils/colorScheme';
import { NATIVE_FILTER_DIVIDER_PREFIX } from '../nativeFilters/FiltersConfigModal/utils';
import { findTabsWithChartsInScope } from '../nativeFilters/utils';
import { getRootLevelTabsComponent } from './utils';
@@ -88,6 +88,14 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const chartIds = useSelector<RootState, number[]>(state =>
Object.values(state.charts).map(chart => chart.id),
);
const renderedChartIds = useSelector<RootState, number[]>(state =>
Object.values(state.charts)
.filter(chart => chart.chartStatus === 'rendered')
.map(chart => chart.id),
);
const [dashboardLabelsColorInitiated, setDashboardLabelsColorInitiated] =
useState(false);
const prevRenderedChartIds = useRef<number[]>([]);
const prevTabIndexRef = useRef();
const tabIndex = useMemo(() => {
@@ -140,28 +148,65 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const activeKey = min === 0 ? DASHBOARD_GRID_ID : min.toString();
const TOP_OF_PAGE_RANGE = 220;
const onBeforeUnload = useCallback(() => {
dispatch(persistDashboardLabelsColor());
resetColors(getColorNamespace(dashboardInfo?.metadata?.color_namespace));
prevRenderedChartIds.current = [];
}, [dashboardInfo?.metadata?.color_namespace, dispatch]);
useEffect(() => {
// verify freshness of color map on tab change
// and when loading for first time
setTimeout(() => {
dispatch(updateDashboardLabelsColor());
}, 500);
}, [directPathToChild, dispatch]);
// verify freshness of color map
// when charts render to catch new labels
const numRenderedCharts = renderedChartIds.length;
if (
dashboardLabelsColorInitiated &&
dashboardInfo?.id &&
numRenderedCharts > 0 &&
prevRenderedChartIds.current.length < numRenderedCharts
) {
const newRenderedChartIds = renderedChartIds.filter(
id => !prevRenderedChartIds.current.includes(id),
);
prevRenderedChartIds.current = renderedChartIds;
dispatch(updateDashboardLabelsColor(newRenderedChartIds));
}
}, [
dashboardInfo?.id,
renderedChartIds,
dispatch,
dashboardLabelsColorInitiated,
]);
useEffect(() => {
const labelsColorMap = getLabelsColorMap();
const colorNamespace = getColorNamespace(
dashboardInfo?.metadata?.color_namespace,
);
labelsColorMap.source = LabelsColorMapSource.Dashboard;
// apply labels color as dictated by stored metadata
applyColors(dashboardInfo.metadata);
if (dashboardInfo?.id && !dashboardLabelsColorInitiated) {
// apply labels color as dictated by stored metadata (if any)
setDashboardLabelsColorInitiated(true);
dispatch(applyDashboardLabelsColorOnLoad(dashboardInfo.metadata));
}
return () => {
resetColors(getColorNamespace(colorNamespace));
onBeforeUnload();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardInfo.id, dispatch]);
}, [dashboardInfo?.id, dispatch]);
useEffect(() => {
// 'beforeunload' event interferes with Cypress data cleanup process.
// This code prevents 'beforeunload' from triggering in Cypress tests,
// as it is not required for end-to-end testing scenarios.
if (!(window as any).Cypress) {
window.addEventListener('beforeunload', onBeforeUnload);
}
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);
};
}, [onBeforeUnload]);
return (
<div className="grid-container" data-test="grid-container">

View File

@@ -16,19 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import * as redux from 'redux';
import { render, screen, fireEvent } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import { getExtensionsRegistry } from '@superset-ui/core';
import { getExtensionsRegistry, JsonObject } from '@superset-ui/core';
import setupExtensions from 'src/setup/setupExtensions';
import getOwnerName from 'src/utils/getOwnerName';
import { HeaderProps } from './types';
import Header from '.';
import { DASHBOARD_HEADER_ID } from '../../util/constants';
const createProps = () => ({
addSuccessToast: jest.fn(),
addDangerToast: jest.fn(),
addWarningToast: jest.fn(),
const initialState = {
dashboardInfo: {
id: 1,
dash_edit_perm: false,
@@ -69,74 +67,69 @@ const createProps = () => ({
userId: 1,
username: 'admin',
},
reports: {},
dashboardTitle: 'Dashboard Title',
dashboardState: {
sliceIds: [],
expandedSlices: {},
refreshFrequency: 0,
shouldPersistRefreshFrequency: false,
css: '',
isStarred: false,
isPublished: false,
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
editMode: false,
lastModifiedTime: 0,
},
charts: {},
layout: {},
expandedSlices: {},
css: '',
customCss: '',
isStarred: false,
isLoading: false,
lastModifiedTime: 0,
refreshFrequency: 0,
shouldPersistRefreshFrequency: false,
onSave: jest.fn(),
onChange: jest.fn(),
fetchFaveStar: jest.fn(),
fetchCharts: jest.fn(),
onRefresh: jest.fn(),
saveFaveStar: jest.fn(),
savePublished: jest.fn(),
isPublished: false,
updateDashboardTitle: jest.fn(),
editMode: false,
setEditMode: jest.fn(),
showBuilderPane: jest.fn(),
updateCss: jest.fn(),
setColorScheme: jest.fn(),
setUnsavedChanges: jest.fn(),
logEvent: jest.fn(),
setRefreshFrequency: jest.fn(),
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
onUndo: jest.fn(),
onRedo: jest.fn(),
undoLength: 0,
redoLength: 0,
setMaxUndoHistoryExceeded: jest.fn(),
maxUndoHistoryToast: jest.fn(),
dashboardInfoChanged: jest.fn(),
dashboardTitleChanged: jest.fn(),
showMenuDropdown: true,
});
const props = createProps();
const editableProps = {
...props,
editMode: true,
dashboardLayout: {
present: {
[DASHBOARD_HEADER_ID]: {
meta: {
text: 'Dashboard Title',
},
},
},
past: [],
future: [],
},
};
const editableState = {
dashboardState: {
...initialState.dashboardState,
editMode: true,
},
dashboardInfo: {
...props.dashboardInfo,
...initialState.dashboardInfo,
dash_edit_perm: true,
dash_save_perm: true,
},
};
const undoProps = {
...editableProps,
undoLength: 1,
const undoState = {
...editableState,
dashboardLayout: {
...initialState.dashboardLayout,
past: [{}],
},
};
const redoProps = {
...editableProps,
redoLength: 1,
const redoState = {
...editableState,
dashboardLayout: {
...initialState.dashboardLayout,
future: [{}],
},
};
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
function setup(props: HeaderProps, initialState = {}) {
function setup(overrideState: JsonObject = {}) {
return render(
<div className="dashboard">
<Header {...props} />
<Header />
</div>,
{ useRedux: true, initialState },
{ useRedux: true, initialState: { ...initialState, ...overrideState } },
);
}
@@ -146,173 +139,223 @@ async function openActionsDropdown() {
expect(await screen.findByTestId('header-actions-menu')).toBeInTheDocument();
}
const addSuccessToast = jest.fn();
const addDangerToast = jest.fn();
const addWarningToast = jest.fn();
const onUndo = jest.fn();
const onRedo = jest.fn();
const setEditMode = jest.fn();
const setUnsavedChanges = jest.fn();
const fetchFaveStar = jest.fn();
const saveFaveStar = jest.fn();
const savePublished = jest.fn();
const fetchCharts = jest.fn();
const updateDashboardTitle = jest.fn();
const updateCss = jest.fn();
const onChange = jest.fn();
const onSave = jest.fn();
const setMaxUndoHistoryExceeded = jest.fn();
const maxUndoHistoryToast = jest.fn();
const logEvent = jest.fn();
const setRefreshFrequency = jest.fn();
const onRefresh = jest.fn();
const dashboardInfoChanged = jest.fn();
const dashboardTitleChanged = jest.fn();
beforeAll(() => {
jest.spyOn(redux, 'bindActionCreators').mockImplementation(() => ({
addSuccessToast,
addDangerToast,
addWarningToast,
onUndo,
onRedo,
setEditMode,
setUnsavedChanges,
fetchFaveStar,
saveFaveStar,
savePublished,
fetchCharts,
updateDashboardTitle,
updateCss,
onChange,
onSave,
setMaxUndoHistoryExceeded,
maxUndoHistoryToast,
logEvent,
setRefreshFrequency,
onRefresh,
dashboardInfoChanged,
dashboardTitleChanged,
}));
});
beforeEach(() => {
jest.clearAllMocks();
});
test('should render', () => {
const mockedProps = createProps();
const { container } = setup(mockedProps);
const { container } = setup();
expect(container).toBeInTheDocument();
});
test('should render the title', () => {
const mockedProps = createProps();
setup(mockedProps);
setup();
expect(screen.getByTestId('editable-title')).toHaveTextContent(
'Dashboard Title',
);
});
test('should render the editable title', () => {
setup(editableProps);
setup(editableState);
expect(screen.getByDisplayValue('Dashboard Title')).toBeInTheDocument();
});
test('should edit the title', () => {
setup(editableProps);
setup(editableState);
const editableTitle = screen.getByDisplayValue('Dashboard Title');
expect(editableProps.onChange).not.toHaveBeenCalled();
expect(onChange).not.toHaveBeenCalled();
userEvent.click(editableTitle);
userEvent.clear(editableTitle);
userEvent.type(editableTitle, 'New Title');
userEvent.click(document.body);
expect(editableProps.onChange).toHaveBeenCalled();
expect(onChange).toHaveBeenCalled();
expect(screen.getByDisplayValue('New Title')).toBeInTheDocument();
});
test('should render the "Draft" status', () => {
const mockedProps = createProps();
setup(mockedProps);
setup();
expect(screen.getByText('Draft')).toBeInTheDocument();
});
test('should publish', () => {
const mockedProps = createProps();
const canEditProps = {
...mockedProps,
const canEditState = {
dashboardInfo: {
...mockedProps.dashboardInfo,
...initialState.dashboardInfo,
dash_edit_perm: true,
dash_save_perm: true,
},
};
setup(canEditProps);
setup(canEditState);
const draft = screen.getByText('Draft');
expect(mockedProps.savePublished).toHaveBeenCalledTimes(0);
expect(savePublished).toHaveBeenCalledTimes(0);
userEvent.click(draft);
expect(mockedProps.savePublished).toHaveBeenCalledTimes(1);
expect(savePublished).toHaveBeenCalledTimes(1);
});
test('should render metadata', () => {
const mockedProps = createProps();
setup(mockedProps);
setup();
expect(
screen.getByText(getOwnerName(mockedProps.dashboardInfo.created_by)),
screen.getByText(getOwnerName(initialState.dashboardInfo.created_by)),
).toBeInTheDocument();
expect(
screen.getByText(mockedProps.dashboardInfo.changed_on_delta_humanized),
screen.getByText(initialState.dashboardInfo.changed_on_delta_humanized),
).toBeInTheDocument();
});
test('should render the "Undo" action as disabled', () => {
setup(editableProps);
setup(editableState);
expect(screen.getByTestId('undo-action').parentElement).toBeDisabled();
});
test('should undo', () => {
setup(undoProps);
setup(undoState);
const undo = screen.getByTestId('undo-action');
expect(undoProps.onUndo).not.toHaveBeenCalled();
expect(onUndo).not.toHaveBeenCalled();
userEvent.click(undo);
expect(undoProps.onUndo).toHaveBeenCalledTimes(1);
expect(onUndo).toHaveBeenCalledTimes(1);
});
test('should undo with key listener', () => {
undoProps.onUndo.mockReset();
setup(undoProps);
expect(undoProps.onUndo).not.toHaveBeenCalled();
onUndo.mockReset();
setup(undoState);
expect(onUndo).not.toHaveBeenCalled();
fireEvent.keyDown(document.body, { key: 'z', code: 'KeyZ', ctrlKey: true });
expect(undoProps.onUndo).toHaveBeenCalledTimes(1);
expect(onUndo).toHaveBeenCalledTimes(1);
});
test('should render the "Redo" action as disabled', () => {
setup(editableProps);
setup(editableState);
expect(screen.getByTestId('redo-action').parentElement).toBeDisabled();
});
test('should redo', () => {
setup(redoProps);
setup(redoState);
const redo = screen.getByTestId('redo-action');
expect(redoProps.onRedo).not.toHaveBeenCalled();
expect(onRedo).not.toHaveBeenCalled();
userEvent.click(redo);
expect(redoProps.onRedo).toHaveBeenCalledTimes(1);
expect(onRedo).toHaveBeenCalledTimes(1);
});
test('should redo with key listener', () => {
redoProps.onRedo.mockReset();
setup(redoProps);
expect(redoProps.onRedo).not.toHaveBeenCalled();
setup(redoState);
expect(onRedo).not.toHaveBeenCalled();
fireEvent.keyDown(document.body, { key: 'y', code: 'KeyY', ctrlKey: true });
expect(redoProps.onRedo).toHaveBeenCalledTimes(1);
expect(onRedo).toHaveBeenCalledTimes(1);
});
test('should render the "Discard changes" button', () => {
setup(editableProps);
setup(editableState);
expect(screen.getByText('Discard')).toBeInTheDocument();
});
test('should render the "Save" button as disabled', () => {
setup(editableProps);
setup(editableState);
expect(screen.getByText('Save').parentElement).toBeDisabled();
});
test('should save', () => {
const unsavedProps = {
...editableProps,
hasUnsavedChanges: true,
const unsavedState = {
...editableState,
dashboardState: {
...editableState.dashboardState,
hasUnsavedChanges: true,
},
};
setup(unsavedProps);
setup(unsavedState);
const save = screen.getByText('Save');
expect(unsavedProps.onSave).not.toHaveBeenCalled();
expect(onSave).not.toHaveBeenCalled();
userEvent.click(save);
expect(unsavedProps.onSave).toHaveBeenCalledTimes(1);
expect(onSave).toHaveBeenCalledTimes(1);
});
test('should NOT render the "Draft" status', () => {
const mockedProps = createProps();
const publishedProps = {
...mockedProps,
isPublished: true,
const publishedState = {
...initialState,
dashboardState: {
...initialState.dashboardState,
isPublished: true,
},
};
setup(publishedProps);
setup(publishedState);
expect(screen.queryByText('Draft')).not.toBeInTheDocument();
});
test('should render the unselected fave icon', () => {
const mockedProps = createProps();
setup(mockedProps);
expect(mockedProps.fetchFaveStar).toHaveBeenCalled();
setup();
expect(fetchFaveStar).toHaveBeenCalled();
expect(
screen.getByRole('img', { name: 'favorite-unselected' }),
).toBeInTheDocument();
});
test('should render the selected fave icon', () => {
const mockedProps = createProps();
const favedProps = {
...mockedProps,
isStarred: true,
const favedState = {
dashboardState: {
...initialState.dashboardState,
isStarred: true,
},
};
setup(favedProps);
setup(favedState);
expect(
screen.getByRole('img', { name: 'favorite-selected' }),
).toBeInTheDocument();
});
test('should NOT render the fave icon on anonymous user', () => {
const mockedProps = createProps();
const anonymousUserProps = {
...mockedProps,
const anonymousUserState = {
user: undefined,
};
setup(anonymousUserProps);
setup(anonymousUserState);
expect(() =>
screen.getByRole('img', { name: 'favorite-unselected' }),
).toThrow('Unable to find');
@@ -322,42 +365,37 @@ test('should NOT render the fave icon on anonymous user', () => {
});
test('should fave', async () => {
const mockedProps = createProps();
setup(mockedProps);
setup();
const fave = screen.getByRole('img', { name: 'favorite-unselected' });
expect(mockedProps.saveFaveStar).not.toHaveBeenCalled();
expect(saveFaveStar).not.toHaveBeenCalled();
userEvent.click(fave);
expect(mockedProps.saveFaveStar).toHaveBeenCalledTimes(1);
expect(saveFaveStar).toHaveBeenCalledTimes(1);
});
test('should toggle the edit mode', () => {
const mockedProps = createProps();
const canEditProps = {
...mockedProps,
const canEditState = {
dashboardInfo: {
...mockedProps.dashboardInfo,
...initialState.dashboardInfo,
dash_edit_perm: true,
},
};
setup(canEditProps);
setup(canEditState);
const editDashboard = screen.getByText('Edit dashboard');
expect(screen.queryByText('Edit dashboard')).toBeInTheDocument();
userEvent.click(editDashboard);
expect(mockedProps.logEvent).toHaveBeenCalled();
expect(logEvent).toHaveBeenCalled();
});
test('should render the dropdown icon', () => {
const mockedProps = createProps();
setup(mockedProps);
setup();
expect(screen.getByRole('img', { name: 'more-horiz' })).toBeInTheDocument();
});
test('should refresh the charts', async () => {
const mockedProps = createProps();
setup(mockedProps);
setup();
await openActionsDropdown();
userEvent.click(screen.getByText('Refresh dashboard'));
expect(mockedProps.onRefresh).toHaveBeenCalledTimes(1);
expect(onRefresh).toHaveBeenCalledTimes(1);
});
test('should render an extension component if one is supplied', () => {
@@ -367,54 +405,48 @@ test('should render an extension component if one is supplied', () => {
));
setupExtensions();
const mockedProps = createProps();
setup(mockedProps);
setup();
expect(
screen.getByText('dashboard.nav.right extension component'),
).toBeInTheDocument();
});
test('should NOT render MetadataBar when in edit mode', () => {
const mockedProps = {
...createProps(),
editMode: true,
const state = {
...editableState,
dashboardInfo: {
...createProps().dashboardInfo,
...initialState.dashboardInfo,
userId: '123',
},
};
setup(mockedProps);
setup(state);
expect(
screen.queryByText(mockedProps.dashboardInfo.changed_on_delta_humanized),
screen.queryByText(state.dashboardInfo.changed_on_delta_humanized),
).not.toBeInTheDocument();
});
test('should NOT render MetadataBar when embedded', () => {
const mockedProps = {
...createProps(),
editMode: false,
const state = {
dashboardInfo: {
...createProps().dashboardInfo,
...initialState.dashboardInfo,
userId: undefined,
},
};
setup(mockedProps);
setup(state);
expect(
screen.queryByText(mockedProps.dashboardInfo.changed_on_delta_humanized),
screen.queryByText(state.dashboardInfo.changed_on_delta_humanized),
).not.toBeInTheDocument();
});
test('should render MetadataBar when not in edit mode and not embedded', () => {
const mockedProps = {
...createProps(),
editMode: false,
const state = {
dashboardInfo: {
...createProps().dashboardInfo,
...initialState.dashboardInfo,
userId: '123',
},
};
setup(mockedProps);
setup(state);
expect(
screen.getByText(mockedProps.dashboardInfo.changed_on_delta_humanized),
screen.getByText(state.dashboardInfo.changed_on_delta_humanized),
).toBeInTheDocument();
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
/**
* 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 { useMemo } from 'react';
import { t } from '@superset-ui/core';
import { DashboardInfo } from 'src/dashboard/types';
import MetadataBar, { MetadataType } from 'src/components/MetadataBar';
import getOwnerName from 'src/utils/getOwnerName';
export const useDashboardMetadataBar = (dashboardInfo: DashboardInfo) => {
const items = useMemo(
() => [
{
type: MetadataType.LastModified as const,
value: dashboardInfo.changed_on_delta_humanized,
modifiedBy:
getOwnerName(dashboardInfo.changed_by) || t('Not available'),
},
{
type: MetadataType.Owner as const,
createdBy: getOwnerName(dashboardInfo.created_by) || t('Not available'),
owners:
dashboardInfo.owners.length > 0
? dashboardInfo.owners.map(getOwnerName)
: t('None'),
createdOn: dashboardInfo.created_on_delta_humanized,
},
],
[
dashboardInfo.changed_by,
dashboardInfo.changed_on_delta_humanized,
dashboardInfo.created_by,
dashboardInfo.created_on_delta_humanized,
dashboardInfo.owners,
],
);
return <MetadataBar items={items} tooltipPlacement="bottom" />;
};

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { omit } from 'lodash';
import { Input } from 'src/components/Input';
import { FormItem } from 'src/components/Form';
@@ -44,9 +44,19 @@ import withToasts from 'src/components/MessageToasts/withToasts';
import TagType from 'src/types/TagType';
import { fetchTags, OBJECT_TYPES } from 'src/features/tags/tags';
import { loadTags } from 'src/components/Tags/utils';
import { applyColors, getColorNamespace } from 'src/utils/colorScheme';
import {
applyColors,
getColorNamespace,
getLabelsColorMapEntries,
} from 'src/utils/colorScheme';
import getOwnerName from 'src/utils/getOwnerName';
import Owner from 'src/types/Owner';
import { useDispatch } from 'react-redux';
import {
setColorScheme,
setDashboardMetadata,
} from 'src/dashboard/actions/dashboardState';
import { areObjectsEqual } from 'src/reduxUtils';
const StyledFormItem = styled(FormItem)`
margin-bottom: 0;
@@ -84,6 +94,7 @@ type DashboardInfo = {
certifiedBy: string;
certificationDetails: string;
isManagedExternally: boolean;
metadata: Record<string, any>;
};
const PropertiesModal = ({
@@ -98,10 +109,11 @@ const PropertiesModal = ({
onSubmit = () => {},
show = false,
}: PropertiesModalProps) => {
const dispatch = useDispatch();
const [form] = AntdForm.useForm();
const [isLoading, setIsLoading] = useState(false);
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
const [colorScheme, setColorScheme] = useState(currentColorScheme);
const [colorScheme, setCurrentColorScheme] = useState(currentColorScheme);
const [jsonMetadata, setJsonMetadata] = useState('');
const [dashboardInfo, setDashboardInfo] = useState<DashboardInfo>();
const [owners, setOwners] = useState<Owners>([]);
@@ -109,6 +121,7 @@ const PropertiesModal = ({
const saveLabel = onlyApply ? t('Apply') : t('Save');
const [tags, setTags] = useState<TagType[]>([]);
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
const originalDashboardMetadata = useRef<Record<string, any>>({});
const tagsAsSelectValues = useMemo(() => {
const selectTags = tags.map((tag: { id: number; name: string }) => ({
@@ -182,21 +195,24 @@ const PropertiesModal = ({
certifiedBy: certified_by || '',
certificationDetails: certification_details || '',
isManagedExternally: is_managed_externally || false,
metadata,
};
form.setFieldsValue(dashboardInfo);
setDashboardInfo(dashboardInfo);
setOwners(owners);
setRoles(roles);
setColorScheme(metadata.color_scheme);
setCurrentColorScheme(metadata.color_scheme);
const metaDataCopy = omit(metadata, [
'positions',
'shared_label_colors',
'map_label_colors',
'color_scheme_domain',
]);
setJsonMetadata(metaDataCopy ? jsonStringify(metaDataCopy) : '');
originalDashboardMetadata.current = metadata;
},
[form],
);
@@ -269,6 +285,8 @@ const PropertiesModal = ({
return parsedRoles;
};
const handleOnCancel = () => onHide();
const onColorSchemeChange = (
colorScheme = '',
{ updateMetadata = true } = {},
@@ -287,20 +305,21 @@ const PropertiesModal = ({
throw new Error('A valid color scheme is required');
}
jsonMetadataObj.color_scheme = colorScheme;
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
setCurrentColorScheme(colorScheme);
dispatch(setColorScheme(colorScheme));
// update metadata to match selection
if (updateMetadata) {
jsonMetadataObj.color_scheme = colorScheme;
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
setJsonMetadata(jsonStringify(jsonMetadataObj));
}
setColorScheme(colorScheme);
};
const onFinish = () => {
const { title, slug, certifiedBy, certificationDetails } =
form.getFieldsValue();
let currentColorScheme = colorScheme;
let currentJsonMetadata = jsonMetadata;
// validate currentJsonMetadata
@@ -318,29 +337,48 @@ const PropertiesModal = ({
return;
}
const copyMetadata = { ...metadata };
const colorNamespace = getColorNamespace(metadata?.color_namespace);
// color scheme in json metadata has precedence over selection
currentColorScheme = metadata?.color_scheme || colorScheme;
const updatedColorScheme = metadata?.color_scheme || colorScheme;
const shouldGoFresh =
updatedColorScheme !== originalDashboardMetadata.current.color_scheme;
const shouldResetCustomLabels = !areObjectsEqual(
originalDashboardMetadata.current.label_colors || {},
metadata?.label_colors || {},
);
const currentCustomLabels = Object.keys(metadata?.label_colors || {});
const prevCustomLabels = Object.keys(
originalDashboardMetadata.current.label_colors || {},
);
const resettableCustomLabels =
currentCustomLabels.length > 0 ? currentCustomLabels : prevCustomLabels;
const freshCustomLabels =
shouldResetCustomLabels && resettableCustomLabels.length > 0
? resettableCustomLabels
: false;
const jsonMetadataObj = getJsonMetadata();
const customLabelColors = jsonMetadataObj.label_colors || {};
const updatedDashboardMetadata = {
...originalDashboardMetadata.current,
label_colors: customLabelColors,
color_scheme: updatedColorScheme,
};
// remove information from user facing input
if (metadata?.shared_label_colors) {
delete metadata.shared_label_colors;
}
if (metadata?.color_scheme_domain) {
delete metadata.color_scheme_domain;
}
originalDashboardMetadata.current = updatedDashboardMetadata;
applyColors(updatedDashboardMetadata, shouldGoFresh || freshCustomLabels);
dispatch(
setDashboardMetadata({
...updatedDashboardMetadata,
map_label_colors: getLabelsColorMapEntries(customLabelColors),
}),
);
// only apply colors, the user has not saved yet
applyColors(copyMetadata, true);
currentJsonMetadata = jsonStringify(metadata);
onColorSchemeChange(currentColorScheme, {
onColorSchemeChange(updatedColorScheme, {
updateMetadata: false,
});
currentJsonMetadata = jsonStringify(metadata);
const moreOnSubmitProps: { roles?: Roles } = {};
const morePutProps: { roles?: number[]; tags?: (number | undefined)[] } =
{};
@@ -557,14 +595,14 @@ const PropertiesModal = ({
return (
<Modal
show={show}
onHide={onHide}
onHide={handleOnCancel}
title={t('Dashboard properties')}
footer={
<>
<Button
htmlType="button"
buttonSize="small"
onClick={onHide}
onClick={handleOnCancel}
data-test="properties-modal-cancel-button"
cta
>

View File

@@ -66,8 +66,8 @@ const SyncDashboardState: FC<Props> = ({ dashboardPageId }) => {
>(
({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
labelsColor: dashboardInfo.metadata?.label_colors || EMPTY_OBJECT,
labelsColorMap:
dashboardInfo.metadata?.shared_label_colors || EMPTY_OBJECT,
labelsColorMap: dashboardInfo.metadata?.map_label_colors || EMPTY_OBJECT,
sharedLabelsColors: dashboardInfo.metadata?.shared_label_colors || [],
colorScheme: dashboardState?.colorScheme,
chartConfiguration:
dashboardInfo.metadata?.chart_configuration || EMPTY_OBJECT,

View File

@@ -197,11 +197,19 @@ class Chart extends Component {
}
}
} else if (
// chart should re-render if color scheme or label color was changed
// chart should re-render if color scheme or label colors were changed
nextProps.formData?.color_scheme !== this.props.formData?.color_scheme ||
!areObjectsEqual(
nextProps.formData?.label_colors,
this.props.formData?.label_colors,
nextProps.formData?.label_colors || {},
this.props.formData?.label_colors || {},
) ||
!areObjectsEqual(
nextProps.formData?.map_label_colors || {},
this.props.formData?.map_label_colors || {},
) ||
!isEqual(
nextProps.formData?.shared_label_colors || [],
this.props.formData?.shared_label_colors || [],
)
) {
return true;

View File

@@ -33,7 +33,7 @@ export const crossFiltersSelector = (props: {
verboseMaps: { [key: string]: Record<string, string> };
}): CrossFilterIndicator[] => {
const { dataMask, chartConfiguration, dashboardLayout, verboseMaps } = props;
const chartsIds = Object.keys(chartConfiguration);
const chartsIds = Object.keys(chartConfiguration || {});
return chartsIds
.map(chartId => {

View File

@@ -41,7 +41,8 @@ const initialState: { dashboardInfo: DashboardInfo } = {
color_namespace: '',
color_scheme_domain: [],
label_colors: {},
shared_label_colors: {},
shared_label_colors: [],
map_label_colors: {},
cross_filters_enabled: false,
},
json_metadata: '',
@@ -51,6 +52,9 @@ const initialState: { dashboardInfo: DashboardInfo } = {
conf: {},
},
crossFiltersEnabled: true,
created_on_delta_humanized: '',
changed_on_delta_humanized: '',
owners: [],
},
};

View File

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

View File

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

View File

@@ -1,137 +0,0 @@
/**
* 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 { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { updateDataMask } from 'src/dataMask/actions';
import DashboardHeader from 'src/dashboard/components/Header';
import isDashboardLoading from 'src/dashboard/util/isDashboardLoading';
import { dashboardInfoChanged } from 'src/dashboard/actions/dashboardInfo';
import {
setEditMode,
showBuilderPane,
fetchFaveStar,
saveFaveStar,
savePublished,
setColorScheme,
setUnsavedChanges,
fetchCharts,
updateCss,
onChange,
saveDashboardRequest,
setMaxUndoHistoryExceeded,
maxUndoHistoryToast,
setRefreshFrequency,
onRefresh,
} from 'src/dashboard/actions/dashboardState';
import {
undoLayoutAction,
redoLayoutAction,
updateDashboardTitle,
dashboardTitleChanged,
} from 'src/dashboard/actions/dashboardLayout';
import {
addSuccessToast,
addDangerToast,
addWarningToast,
} from 'src/components/MessageToasts/actions';
import { logEvent } from 'src/logger/actions';
import { DASHBOARD_HEADER_ID } from 'src/dashboard/util/constants';
function mapStateToProps({
dashboardLayout: undoableLayout,
dashboardState,
reports,
dashboardInfo,
charts,
dataMask,
user,
}) {
return {
dashboardInfo,
undoLength: undoableLayout.past.length,
redoLength: undoableLayout.future.length,
layout: undoableLayout.present,
dashboardTitle: (
(undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {}
).text,
expandedSlices: dashboardState.expandedSlices,
refreshFrequency: dashboardState.refreshFrequency,
shouldPersistRefreshFrequency:
!!dashboardState.shouldPersistRefreshFrequency,
customCss: dashboardState.css,
colorNamespace: dashboardState.colorNamespace,
colorScheme: dashboardState.colorScheme,
charts,
dataMask,
user,
isStarred: !!dashboardState.isStarred,
isPublished: !!dashboardState.isPublished,
isLoading: isDashboardLoading(charts),
hasUnsavedChanges: !!dashboardState.hasUnsavedChanges,
maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
lastModifiedTime: Math.max(
dashboardState.lastModifiedTime,
dashboardInfo.last_modified_time,
),
editMode: !!dashboardState.editMode,
slug: dashboardInfo.slug,
metadata: dashboardInfo.metadata,
reports,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
addSuccessToast,
addDangerToast,
addWarningToast,
onUndo: undoLayoutAction,
onRedo: redoLayoutAction,
setEditMode,
showBuilderPane,
setColorScheme,
setUnsavedChanges,
fetchFaveStar,
saveFaveStar,
savePublished,
fetchCharts,
updateDashboardTitle,
updateCss,
onChange,
onSave: saveDashboardRequest,
setMaxUndoHistoryExceeded,
maxUndoHistoryToast,
logEvent,
setRefreshFrequency,
onRefresh,
dashboardInfoChanged,
dashboardTitleChanged,
updateDataMask,
},
dispatch,
);
}
export default connect(mapStateToProps, mapDispatchToProps)(DashboardHeader);

View File

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

View File

@@ -33,6 +33,7 @@ import Database from 'src/types/Database';
import { UrlParamEntries } from 'src/utils/urlUtils';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import Owner from 'src/types/Owner';
import { ChartState } from '../explore/types';
export type { Dashboard } from 'src/types/Dashboard';
@@ -52,6 +53,8 @@ export type Chart = ChartState & {
form_data: {
viz_type: string;
datasource: string;
color_scheme: string;
slice_id: number;
};
};
@@ -131,11 +134,17 @@ 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;
filterBarOrientation: FilterBarOrientation;
created_on_delta_humanized: string;
changed_on_delta_humanized: string;
changed_by?: Owner;
created_by?: Owner;
owners: Owner[];
};
export type ChartsState = { [key: string]: Chart };

View File

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

View File

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

View File

@@ -16,12 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { Tooltip } from 'src/components/Tooltip';
import { css, logging, SupersetClient, t, tn } from '@superset-ui/core';
import { css, logging, SupersetClient, t } from '@superset-ui/core';
import { chartPropShape } from 'src/dashboard/util/propShapes';
import AlteredSliceTag from 'src/components/AlteredSliceTag';
import Button from 'src/components/Button';
@@ -29,16 +29,17 @@ import Icons from 'src/components/Icons';
import PropertiesModal from 'src/explore/components/PropertiesModal';
import { sliceUpdated } from 'src/explore/actions/exploreActions';
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
import MetadataBar, { MetadataType } from 'src/components/MetadataBar';
import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions';
import { applyColors, resetColors } from 'src/utils/colorScheme';
import { useExploreAdditionalActionsMenu } from '../useExploreAdditionalActionsMenu';
import { useExploreMetadataBar } from './useExploreMetadataBar';
const propTypes = {
actions: PropTypes.object.isRequired,
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'));
@@ -159,48 +160,7 @@ export const ExploreChartHeader = ({
metadata?.dashboards,
);
const metadataBar = useMemo(() => {
if (!metadata) {
return null;
}
const items = [];
items.push({
type: MetadataType.Dashboards,
title:
metadata.dashboards.length > 0
? tn(
'Added to 1 dashboard',
'Added to %s dashboards',
metadata.dashboards.length,
metadata.dashboards.length,
)
: t('Not added to any dashboard'),
description:
metadata.dashboards.length > 0
? t(
'You can preview the list of dashboards in the chart settings dropdown.',
)
: undefined,
});
items.push({
type: MetadataType.LastModified,
value: metadata.changed_on_humanized,
modifiedBy: metadata.changed_by || t('Not available'),
});
items.push({
type: MetadataType.Owner,
createdBy: metadata.created_by || t('Not available'),
owners: metadata.owners.length > 0 ? metadata.owners : t('None'),
createdOn: metadata.created_on_humanized,
});
if (slice?.description) {
items.push({
type: MetadataType.Description,
value: slice?.description,
});
}
return <MetadataBar items={items} tooltipPlacement="bottom" />;
}, [metadata, slice?.description]);
const metadataBar = useExploreMetadataBar(metadata, slice);
const oldSliceName = slice?.slice_name;
return (

View File

@@ -0,0 +1,71 @@
/**
* 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 { useMemo } from 'react';
import { t, tn } from '@superset-ui/core';
import MetadataBar, { MetadataType } from 'src/components/MetadataBar';
import { ExplorePageInitialData } from 'src/explore/types';
export const useExploreMetadataBar = (
metadata: ExplorePageInitialData['metadata'],
slice: ExplorePageInitialData['slice'],
) =>
useMemo(() => {
if (!metadata) {
return null;
}
const items = [];
if (metadata.dashboards) {
items.push({
type: MetadataType.Dashboards as const,
title:
metadata.dashboards.length > 0
? tn(
'Added to 1 dashboard',
'Added to %s dashboards',
metadata.dashboards.length,
metadata.dashboards.length,
)
: t('Not added to any dashboard'),
description:
metadata.dashboards.length > 0
? t(
'You can preview the list of dashboards in the chart settings dropdown.',
)
: undefined,
});
}
items.push({
type: MetadataType.LastModified as const,
value: metadata.changed_on_humanized,
modifiedBy: metadata.changed_by || t('Not available'),
});
items.push({
type: MetadataType.Owner as const,
createdBy: metadata.created_by || t('Not available'),
owners: metadata.owners.length > 0 ? metadata.owners : t('None'),
createdOn: metadata.created_on_humanized,
});
if (slice?.description) {
items.push({
type: MetadataType.Description as const,
value: slice?.description,
});
}
return <MetadataBar items={items} tooltipPlacement="bottom" />;
}, [metadata, slice?.description]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,6 +82,10 @@ export interface ExplorePageInitialData {
owners: string[];
created_by?: string;
changed_by?: string;
dashboards?: {
id: number;
dashboard_title: string;
}[];
};
saveAction?: SaveActionType | null;
}

View File

@@ -99,9 +99,10 @@ const getDashboardContextFormData = () => {
if (dashboardContext) {
const sliceId = getUrlParam(URL_PARAMS.sliceId) || 0;
const {
colorScheme,
labelsColor,
labelsColorMap,
colorScheme,
sharedLabelsColors,
chartConfiguration,
nativeFilters,
filterBoxFilters,
@@ -113,15 +114,18 @@ const getDashboardContextFormData = () => {
filters: getAppliedFilterValues(sliceId, filterBoxFilters),
nativeFilters,
chartConfiguration,
colorScheme,
dataMask,
colorScheme,
labelsColor,
labelsColorMap,
sharedLabelsColors,
sliceId,
allSliceIds: [sliceId],
extraControls: {},
});
Object.assign(dashboardContextWithFilters, { dashboardId });
Object.assign(dashboardContextWithFilters, {
dashboardId,
});
return dashboardContextWithFilters;
}
return null;

View File

@@ -26,6 +26,7 @@ import { ChartConfiguration } from 'src/dashboard/types';
export interface DashboardContextForExplore {
labelsColor: Record<string, string>;
labelsColorMap: Record<string, string>;
sharedLabelsColors: string[];
colorScheme: string;
chartConfiguration: ChartConfiguration;
nativeFilters: PartialFilters;

View File

@@ -32,16 +32,60 @@ import {
export const getColorNamespace = (namespace?: string) => namespace || undefined;
/**
* Get the labels color map entries
* Get labels shared across all charts in a dashboard.
* Merges a fresh instance of shared label colors with a stored one.
*
* @returns Record<string, string>
*/
export const getLabelsColorMapEntries = (): Record<string, string> => {
const labelsColorMapInstance = getLabelsColorMap();
const updatedLabelsColorMapEntries = Object.fromEntries(
labelsColorMapInstance.getColorMap(),
export const getFreshSharedLabels = (
currentSharedLabels: string[] = [],
): string[] => {
const { chartsLabelsMap } = getLabelsColorMap();
const allLabels = Array.from(chartsLabelsMap.values()).flatMap(
({ labels }) => labels,
);
return updatedLabelsColorMapEntries;
const duplicates = Array.from(
allLabels.reduce(
(counts, label) => counts.set(label, (counts.get(label) || 0) + 1),
new Map(),
),
)
.filter(([, count]) => count > 1)
.map(([label]) => label);
return Array.from(new Set([...currentSharedLabels, ...duplicates]));
};
export const getSharedLabelsColorMapEntries = (
currentColorMap: Record<string, string>,
sharedLabels: string[],
): Record<string, string> =>
Object.fromEntries(
Object.entries(currentColorMap).filter(([label]) =>
sharedLabels.includes(label),
),
);
/**
* Returns all entries (labels and colors) except custom label colors.
*
* @param customLabelsColor - the custom label colors in label_colors field
* @returns all color entries except custom label colors
*/
export const getLabelsColorMapEntries = (
customLabelsColor: Record<string, string>,
): Record<string, string> => {
const labelsColorMapInstance = getLabelsColorMap();
const allEntries = Object.fromEntries(labelsColorMapInstance.getColorMap());
// custom label colors are applied and stored separetely via label_colors
// removing all instances of custom label colors from the entries
Object.keys(customLabelsColor).forEach(label => {
delete allEntries[label];
});
return allEntries;
};
export const getColorSchemeDomain = (colorScheme: string) =>
@@ -56,15 +100,20 @@ export const getColorSchemeDomain = (colorScheme: string) =>
export const isLabelsColorMapSynced = (
metadata: Record<string, any>,
): boolean => {
const currentLabelsColorMap = metadata?.shared_label_colors || {};
const customLabelColors = metadata?.label_colors || {};
const freshLabelsColorMap = getLabelsColorMap().getColorMap();
const isSynced = Array.from(freshLabelsColorMap.entries()).every(
const storedLabelsColorMap = metadata.map_label_colors || {};
const customLabelColors = metadata.label_colors || {};
const freshColorMap = getLabelsColorMap().getColorMap();
const fullFreshColorMap = {
...Object.fromEntries(freshColorMap),
...customLabelColors,
};
const isSynced = Object.entries(fullFreshColorMap).every(
([label, color]) =>
currentLabelsColorMap.hasOwnProperty(label) &&
(currentLabelsColorMap[label] === color ||
customLabelColors[label] !== undefined),
storedLabelsColorMap.hasOwnProperty(label) &&
storedLabelsColorMap[label] === color,
);
return isSynced;
};
@@ -79,7 +128,7 @@ export const resetColors = (color_namespace?: string) => {
getColorNamespace(color_namespace),
);
categoricalNamespace.resetColors();
labelsColorMapInstance.clear();
labelsColorMapInstance.reset();
};
/**
@@ -92,13 +141,18 @@ export const resetColors = (color_namespace?: string) => {
export const refreshLabelsColorMap = (
namespace?: string,
colorScheme?: string,
merge = false,
) => {
const colorNameSpace = getColorNamespace(namespace);
const categoricalNamespace =
CategoricalColorNamespace.getNamespace(colorNameSpace);
const labelsColorMapInstance = getLabelsColorMap();
labelsColorMapInstance.updateColorMap(categoricalNamespace, colorScheme);
labelsColorMapInstance.updateColorMap(
categoricalNamespace,
colorScheme,
merge,
);
};
/**
@@ -107,34 +161,78 @@ export const refreshLabelsColorMap = (
*
* @param metadata - the dashboard metadata object
*/
export const applyColors = (metadata: Record<string, any>, fresh = false) => {
export const applyColors = (
metadata: Record<string, any>,
// Create a fresh color map by changing color scheme
fresh: boolean | string[] = false,
// Catch new labels in the color map as they appear
merge = false,
// Apply only label colors that are shared across multiple charts.
shared = false,
) => {
const colorNameSpace = getColorNamespace(metadata?.color_namespace);
const categoricalNamespace =
CategoricalColorNamespace.getNamespace(colorNameSpace);
const colorScheme = metadata?.color_scheme;
const customLabelColors = metadata?.label_colors || {};
// when scheme unset, update only custom label colors
const labelsColorMap = metadata?.shared_label_colors || {};
const fullLabelsColor = metadata?.map_label_colors || {};
const sharedLabels = metadata?.shared_label_colors || [];
const customLabelsColor = metadata?.label_colors || {};
const sharedLabelsColor = getSharedLabelsColorMapEntries(
fullLabelsColor,
sharedLabels,
);
// reset forced colors (custom labels + labels color map)
categoricalNamespace.resetColors();
if (fresh && !Array.isArray(fresh)) {
// reset custom label colors
// re-evaluate all other label colors
categoricalNamespace.resetColors();
}
// apply custom label colors first
Object.keys(customLabelColors).forEach(label => {
categoricalNamespace.setColor(label, customLabelColors[label]);
});
if (fresh && Array.isArray(fresh)) {
// when a color scheme is not set for the dashboard
// should only reset colors for charts that have changed scheme
// while keeping colors of existing shared label colors intact
// this is used also to reset custom label colors when added or removed
categoricalNamespace.resetColorsForLabels(fresh);
}
// re-instantiate a fresh labels color map based on current scheme
// will consider also just applied custom label colors
refreshLabelsColorMap(metadata?.color_namespace, colorScheme);
if (fresh || merge) {
// re-instantiate a fresh labels color map based on current scheme
// it consider just applied custom label colors if present and all forced colors
// it will merge with the existing color map new labels only when merge is true
refreshLabelsColorMap(metadata?.color_namespace, colorScheme, merge);
}
// get the fresh map that was just updated or existing
const labelsColorMapEntries = fresh
? getLabelsColorMapEntries()
: labelsColorMap;
let applicableColorMapEntries: Record<string, any> = fullLabelsColor;
if (fresh) {
// requires a new map all together
applicableColorMapEntries = {
...getLabelsColorMapEntries(customLabelsColor),
};
}
if (merge) {
// must only add up newly appearing labels
// without overriding existing ones
applicableColorMapEntries = {
...fullLabelsColor,
...getLabelsColorMapEntries(customLabelsColor),
};
}
if (shared) {
// must apply the colors to only shared labels
applicableColorMapEntries = sharedLabelsColor;
}
applicableColorMapEntries = {
...applicableColorMapEntries,
...customLabelsColor,
};
// apply the final color map
Object.keys(labelsColorMapEntries).forEach(label => {
categoricalNamespace.setColor(label, labelsColorMapEntries[label]);
});
if (applicableColorMapEntries) {
Object.keys(applicableColorMapEntries).forEach(label => {
categoricalNamespace.setColor(label, applicableColorMapEntries[label]);
});
}
};

View File

@@ -26,6 +26,7 @@ from superset.commands.chart.importers.v1.utils import import_chart
from superset.commands.database.importers.v1.utils import import_database
from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.commands.importers.v1 import ImportModelsCommand
from superset.commands.utils import update_chart_config_dataset
from superset.connectors.sqla.models import SqlaTable
from superset.daos.chart import ChartDAO
from superset.databases.schemas import ImportV1DatabaseSchema
@@ -86,16 +87,10 @@ class ImportChartsCommand(ImportModelsCommand):
# update datasource id, type, and name
dataset = datasets[config["dataset_uuid"]]
config.update(
{
"datasource_id": dataset.id,
"datasource_type": "table",
"datasource_name": dataset.table_name,
}
)
config["params"].update({"datasource": dataset.uid})
if "query_context" in config:
config["query_context"] = None
dataset_dict = {
"datasource_id": dataset.id,
"datasource_type": "table",
"datasource_name": dataset.table_name,
}
config = update_chart_config_dataset(config, dataset_dict)
import_chart(config, overwrite=overwrite)

View File

@@ -34,6 +34,7 @@ from superset.commands.dashboard.importers.v1.utils import (
from superset.commands.database.importers.v1.utils import import_database
from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.commands.importers.v1 import ImportModelsCommand
from superset.commands.utils import update_chart_config_dataset
from superset.daos.dashboard import DashboardDAO
from superset.dashboards.schemas import ImportV1DashboardSchema
from superset.databases.schemas import ImportV1DatabaseSchema
@@ -113,11 +114,7 @@ class ImportDashboardsCommand(ImportModelsCommand):
):
# update datasource id, type, and name
dataset_dict = dataset_info[config["dataset_uuid"]]
config.update(dataset_dict)
dataset_uid = f"{dataset_dict['datasource_id']}__{dataset_dict['datasource_type']}"
config["params"].update({"datasource": dataset_uid})
if "query_context" in config:
config["query_context"] = None
config = update_chart_config_dataset(config, dataset_dict)
chart = import_chart(config, overwrite=False)
charts.append(chart)

View File

@@ -39,6 +39,7 @@ from superset.commands.importers.v1.utils import (
validate_metadata_type,
)
from superset.commands.query.importers.v1.utils import import_saved_query
from superset.commands.utils import update_chart_config_dataset
from superset.dashboards.schemas import ImportV1DashboardSchema
from superset.databases.schemas import ImportV1DatabaseSchema
from superset.datasets.schemas import ImportV1DatasetSchema
@@ -113,11 +114,7 @@ class ImportAssetsCommand(BaseCommand):
for file_name, config in configs.items():
if file_name.startswith("charts/"):
dataset_dict = dataset_info[config["dataset_uuid"]]
config.update(dataset_dict)
dataset_uid = f"{dataset_dict['datasource_id']}__{dataset_dict['datasource_type']}"
config["params"].update({"datasource": dataset_uid})
if "query_context" in config:
config["query_context"] = None
config = update_chart_config_dataset(config, dataset_dict)
chart = import_chart(config, overwrite=True)
charts.append(chart)
chart_ids[str(chart.uuid)] = chart.id

View File

@@ -17,7 +17,7 @@
from __future__ import annotations
from collections import Counter
from typing import Optional, TYPE_CHECKING
from typing import Any, Optional, TYPE_CHECKING
from flask import g
from flask_appbuilder.security.sqla.models import Role, User
@@ -34,6 +34,7 @@ from superset.daos.datasource import DatasourceDAO
from superset.daos.exceptions import DatasourceNotFound
from superset.daos.tag import TagDAO
from superset.tags.models import ObjectType, Tag, TagType
from superset.utils import json
from superset.utils.core import DatasourceType, get_user_id
if TYPE_CHECKING:
@@ -185,3 +186,43 @@ def update_tags(
TagDAO.create_custom_tagged_objects(
object_type, object_id, [tag.name for tag in tags_to_add]
)
def update_chart_config_dataset(
config: dict[str, Any], dataset_info: dict[str, Any]
) -> dict[str, Any]:
"""
Update the chart configuration and query_context with new dataset information
:param config: The original chart configuration
:param dataset_info: Dict with datasource_id, datasource_type, and datasource_name
:return: The updated chart configuration
"""
# Update datasource id, type, and name
config.update(dataset_info)
dataset_uid = f"{dataset_info['datasource_id']}__{dataset_info['datasource_type']}"
config["params"].update({"datasource": dataset_uid})
if "query_context" in config and config["query_context"] is not None:
try:
query_context = json.loads(config["query_context"])
query_context["datasource"] = {
"id": dataset_info["datasource_id"],
"type": dataset_info["datasource_type"],
}
if "form_data" in query_context:
query_context["form_data"]["datasource"] = dataset_uid
if "queries" in query_context:
for query in query_context["queries"]:
if "datasource" in query:
query["datasource"] = query_context["datasource"]
config["query_context"] = json.dumps(query_context)
except json.JSONDecodeError:
config["query_context"] = None
return config

View File

@@ -38,7 +38,8 @@ from superset.exceptions import (
)
from superset.models.core import Database
from superset.result_set import SupersetResultSet
from superset.sql_parse import ParsedQuery, Table
from superset.sql.parse import SQLScript
from superset.sql_parse import Table
from superset.superset_typing import ResultSetColumnType
if TYPE_CHECKING:
@@ -105,8 +106,8 @@ def get_virtual_table_metadata(dataset: SqlaTable) -> list[ResultSetColumnType]:
sql = dataset.get_template_processor().process_template(
dataset.sql, **dataset.template_params_dict
)
parsed_query = ParsedQuery(sql, engine=db_engine_spec.engine)
if not db_engine_spec.is_readonly_query(parsed_query):
parsed_script = SQLScript(sql, engine=db_engine_spec.engine)
if parsed_script.has_mutation():
raise SupersetSecurityException(
SupersetError(
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
@@ -114,8 +115,7 @@ def get_virtual_table_metadata(dataset: SqlaTable) -> list[ResultSetColumnType]:
level=ErrorLevel.ERROR,
)
)
statements = parsed_query.get_statements()
if len(statements) > 1:
if len(parsed_script.statements) > 1:
raise SupersetSecurityException(
SupersetError(
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
@@ -127,7 +127,7 @@ def get_virtual_table_metadata(dataset: SqlaTable) -> list[ResultSetColumnType]:
dataset.database,
dataset.catalog,
dataset.schema,
statements[0],
sql,
)

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