Compare commits
16 Commits
fix-dml
...
npm_audit_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5369ab0f0 | ||
|
|
e0deb704f9 | ||
|
|
abf3790ea6 | ||
|
|
f8adaf66c1 | ||
|
|
832fed1db5 | ||
|
|
ccce9abf57 | ||
|
|
7d60a88048 | ||
|
|
8905508d8f | ||
|
|
0b647b2dcc | ||
|
|
f58b5295a9 | ||
|
|
9e5b568cc9 | ||
|
|
90572be95a | ||
|
|
3c326598a8 | ||
|
|
ae04959175 | ||
|
|
1e224398e1 | ||
|
|
fa7b66424c |
2
.github/actions/chart-releaser-action
vendored
107
.github/workflows/ephemeral-env.yml
vendored
@@ -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.'
|
||||
})
|
||||
|
||||
12
Dockerfile
@@ -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
|
||||
######################################################################
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
BIN
docs/static/img/tutorial/add-data-upload.png
vendored
|
Before Width: | Height: | Size: 90 KiB |
BIN
docs/static/img/tutorial/allow-file-uploads.png
vendored
Normal file
|
After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 248 KiB |
BIN
docs/static/img/tutorial/parse_dates_column.png
vendored
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 93 KiB |
BIN
docs/static/img/tutorial/upload_a_csv.png
vendored
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 70 KiB |
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -91,7 +91,7 @@ export function prepareDashboardFilters(
|
||||
refresh_frequency: 0,
|
||||
color_scheme: '',
|
||||
label_colors: {},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
color_scheme_domain: [],
|
||||
cross_filters_enabled: false,
|
||||
positions: {
|
||||
|
||||
@@ -125,7 +125,7 @@ export const valueNativeFilterOptions = [
|
||||
];
|
||||
|
||||
export function interceptGet() {
|
||||
cy.intercept('/api/v1/dashboard/*').as('get');
|
||||
cy.intercept('GET', '/api/v1/dashboard/*').as('get');
|
||||
}
|
||||
|
||||
export function interceptFiltering() {
|
||||
@@ -144,6 +144,10 @@ export function interceptUpdate() {
|
||||
cy.intercept('PUT', `/api/v1/dashboard/*`).as('update');
|
||||
}
|
||||
|
||||
export function interceptExploreUpdate() {
|
||||
cy.intercept('PUT', `/api/v1/chart/*`).as('chartUpdate');
|
||||
}
|
||||
|
||||
export function interceptPost() {
|
||||
cy.intercept('POST', `/api/v1/dashboard/`).as('post');
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
372
superset-frontend/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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'> = {
|
||||
|
||||
@@ -87,6 +87,9 @@ export interface Dataset {
|
||||
}
|
||||
|
||||
export interface ControlPanelState {
|
||||
slice: {
|
||||
slice_id: number;
|
||||
};
|
||||
form_data: QueryFormData;
|
||||
datasource: Dataset | QueryResponse | null;
|
||||
controls: ControlStateMapping;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export const getColorControlsProps = (state: Record<string, any>) => {
|
||||
const dashboardId = state?.form_data?.dashboardId;
|
||||
return {
|
||||
chartId: state?.slice?.slice_id,
|
||||
dashboardId,
|
||||
hasDashboardColorScheme:
|
||||
!!dashboardId && !!state?.form_data?.dashboard_color_scheme,
|
||||
hasCustomLabelsColor:
|
||||
Object.keys(state?.form_data?.label_colors || {}).length > 0,
|
||||
colorNamespace: state?.form_data?.color_namespace,
|
||||
mapLabelsColors: state?.form_data?.map_label_colors || {},
|
||||
sharedLabelsColors: state?.form_data?.shared_label_colors || [],
|
||||
};
|
||||
};
|
||||
@@ -27,3 +27,4 @@ export * from './defineSavedMetrics';
|
||||
export * from './getStandardizedControls';
|
||||
export * from './getTemporalColumns';
|
||||
export { default as displayTimeRelatedControls } from './displayTimeRelatedControls';
|
||||
export * from './colorControls';
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { getColorControlsProps } from '../../src';
|
||||
|
||||
describe('getColorControlsProps', () => {
|
||||
it('should return default values when state is empty', () => {
|
||||
const state = {};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: undefined,
|
||||
dashboardId: undefined,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: false,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct values when state has form_data with dashboardId and color scheme', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 123,
|
||||
dashboard_color_scheme: 'blueScheme',
|
||||
label_colors: {},
|
||||
},
|
||||
slice: { slice_id: 456 },
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: 456,
|
||||
dashboardId: 123,
|
||||
hasDashboardColorScheme: true,
|
||||
hasCustomLabelsColor: false,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect custom label colors correctly', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 123,
|
||||
label_colors: { label1: '#000000' },
|
||||
},
|
||||
slice: { slice_id: 456 },
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: 456,
|
||||
dashboardId: 123,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: true,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return shared label colors when available', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
shared_label_colors: ['#FF5733', '#33FF57'],
|
||||
},
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: undefined,
|
||||
dashboardId: undefined,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: false,
|
||||
sharedLabelsColors: ['#FF5733', '#33FF57'],
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing form_data and slice properties', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 789,
|
||||
},
|
||||
};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
chartId: undefined,
|
||||
dashboardId: 789,
|
||||
hasDashboardColorScheme: false,
|
||||
hasCustomLabelsColor: false,
|
||||
colorNamespace: undefined,
|
||||
mapLabelsColors: {},
|
||||
sharedLabelsColors: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import CategoricalColorScale from './CategoricalColorScale';
|
||||
import { ColorsLookup } from './types';
|
||||
import getCategoricalSchemeRegistry from './CategoricalSchemeRegistrySingleton';
|
||||
@@ -37,10 +38,21 @@ export default class CategoricalColorNamespace {
|
||||
this.forcedItems = {};
|
||||
}
|
||||
|
||||
getScale(schemeId?: string) {
|
||||
const id = schemeId ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
|
||||
/**
|
||||
* A new CategoricalColorScale instance is created for each chart.
|
||||
*
|
||||
* @param colorScheme - the color scheme to use
|
||||
* @returns a new instance of a color scale
|
||||
*/
|
||||
getScale(colorScheme?: string) {
|
||||
const id =
|
||||
colorScheme ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
|
||||
const scheme = getCategoricalSchemeRegistry().get(id);
|
||||
return new CategoricalColorScale(scheme?.colors ?? [], this.forcedItems);
|
||||
return new CategoricalColorScale(
|
||||
scheme?.colors ?? [],
|
||||
this.forcedItems,
|
||||
colorScheme,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,6 +71,17 @@ export default class CategoricalColorNamespace {
|
||||
resetColors() {
|
||||
this.forcedItems = {};
|
||||
}
|
||||
|
||||
resetColorsForLabels(labels: string[] = []) {
|
||||
const updatedForcedItems = cloneDeep(this.forcedItems);
|
||||
labels.forEach(label => {
|
||||
if (updatedForcedItems.hasOwnProperty(label)) {
|
||||
delete updatedForcedItems[label];
|
||||
}
|
||||
});
|
||||
|
||||
this.forcedItems = { ...updatedForcedItems };
|
||||
}
|
||||
}
|
||||
|
||||
const namespaces: {
|
||||
@@ -80,16 +103,19 @@ export function getNamespace(name: string = DEFAULT_NAMESPACE) {
|
||||
|
||||
export function getColor(
|
||||
value?: string,
|
||||
schemeId?: string,
|
||||
colorScheme?: string,
|
||||
namespace?: string,
|
||||
) {
|
||||
return getNamespace(namespace).getScale(schemeId).getColor(value);
|
||||
return getNamespace(namespace).getScale(colorScheme).getColor(value);
|
||||
}
|
||||
|
||||
/*
|
||||
Returns a new scale instance within the same namespace.
|
||||
Especially useful when a chart is booting for the first time
|
||||
|
||||
@param scheme - the applied color scheme
|
||||
@param namespace - the namespace
|
||||
*/
|
||||
export function getScale(scheme?: string, namespace?: string) {
|
||||
return getNamespace(namespace).getScale(scheme);
|
||||
export function getScale(colorScheme?: string, namespace?: string) {
|
||||
return getNamespace(namespace).getScale(colorScheme);
|
||||
}
|
||||
|
||||
@@ -21,14 +21,16 @@ import { scaleOrdinal, ScaleOrdinal } from 'd3-scale';
|
||||
import { ExtensibleFunction } from '../models';
|
||||
import { ColorsInitLookup, ColorsLookup } from './types';
|
||||
import stringifyAndTrim from './stringifyAndTrim';
|
||||
import getLabelsColorMap from './LabelsColorMapSingleton';
|
||||
import getLabelsColorMap, {
|
||||
LabelsColorMapSource,
|
||||
} from './LabelsColorMapSingleton';
|
||||
import { getAnalogousColors } from './utils';
|
||||
import { FeatureFlag, isFeatureEnabled } from '../utils';
|
||||
|
||||
// Use type augmentation to correct the fact that
|
||||
// an instance of CategoricalScale is also a function
|
||||
interface CategoricalColorScale {
|
||||
(x: { toString(): string }, y?: number, w?: string): string;
|
||||
(x: { toString(): string }, y?: number): string;
|
||||
}
|
||||
|
||||
class CategoricalColorScale extends ExtensibleFunction {
|
||||
@@ -50,11 +52,16 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
* Constructor
|
||||
* @param {*} colors an array of colors
|
||||
* @param {*} forcedColors optional parameter that comes from parent
|
||||
* (usually CategoricalColorNamespace)
|
||||
* @param {*} appliedColorScheme the color scheme applied to the chart
|
||||
*
|
||||
*/
|
||||
constructor(colors: string[], forcedColors: ColorsInitLookup = {}) {
|
||||
super((value: string, sliceId?: number, colorScheme?: string) =>
|
||||
this.getColor(value, sliceId, colorScheme),
|
||||
constructor(
|
||||
colors: string[],
|
||||
forcedColors: ColorsInitLookup = {},
|
||||
appliedColorScheme?: string,
|
||||
) {
|
||||
super((value: string, sliceId?: number) =>
|
||||
this.getColor(value, sliceId, appliedColorScheme),
|
||||
);
|
||||
// holds original color scheme colors
|
||||
this.originColors = colors;
|
||||
@@ -107,15 +114,28 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
*
|
||||
* @param value the value of a label to get the color for
|
||||
* @param sliceId the ID of the current chart
|
||||
* @param colorScheme the original color scheme of the chart
|
||||
* @param appliedColorScheme the color scheme applied to the chart
|
||||
* @returns the color or the next available color
|
||||
*/
|
||||
getColor(value?: string, sliceId?: number, colorScheme?: string): string {
|
||||
getColor(
|
||||
value?: string,
|
||||
sliceId?: number,
|
||||
appliedColorScheme?: string,
|
||||
): string {
|
||||
const cleanedValue = stringifyAndTrim(value);
|
||||
// priority: forced color (i.e. custom label colors) > shared color > scale color
|
||||
// priority: forced color (aka custom label colors) > shared color > scale color
|
||||
const forcedColor = this.forcedColors?.[cleanedValue];
|
||||
const isExistingLabel = this.chartLabelsColorMap.has(cleanedValue);
|
||||
let color = forcedColor || this.scale(cleanedValue);
|
||||
const { source } = this.labelsColorMapInstance;
|
||||
const currentColorMap =
|
||||
source === LabelsColorMapSource.Dashboard
|
||||
? this.labelsColorMapInstance.getColorMap()
|
||||
: this.chartLabelsColorMap;
|
||||
const isExistingLabel = currentColorMap.has(cleanedValue);
|
||||
let color =
|
||||
forcedColor ||
|
||||
(isExistingLabel
|
||||
? (currentColorMap.get(cleanedValue) as string)
|
||||
: this.scale(cleanedValue));
|
||||
|
||||
// a forced color will always be used independently of the usage count
|
||||
if (!forcedColor && !isExistingLabel) {
|
||||
@@ -128,7 +148,7 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
this.isColorUsed(color)
|
||||
) {
|
||||
// fallback to least used color
|
||||
color = this.getNextAvailableColor(color);
|
||||
color = this.getNextAvailableColor(cleanedValue, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +161,7 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
cleanedValue,
|
||||
color,
|
||||
sliceId,
|
||||
colorScheme,
|
||||
appliedColorScheme,
|
||||
);
|
||||
}
|
||||
return color;
|
||||
@@ -164,48 +184,76 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
* @param color the color to check
|
||||
* @returns the count of the color usage in this slice
|
||||
*/
|
||||
getColorUsageCount(currentColor: string): number {
|
||||
let count = 0;
|
||||
this.chartLabelsColorMap.forEach(color => {
|
||||
if (color === currentColor) {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
getColorUsageCount(color: string): number {
|
||||
return Array.from(this.chartLabelsColorMap.values()).filter(
|
||||
value => value === color,
|
||||
).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lower chances of color collision by returning the least used color
|
||||
* Checks across colors of current slice within LabelsColorMapSingleton
|
||||
* Lower chances of color collision by returning the least used color.
|
||||
* Checks across colors of current slice within chartLabelsColorMap.
|
||||
*
|
||||
* @param currentLabel the current label
|
||||
* @param currentColor the current color
|
||||
* @returns the least used color that is not the excluded color
|
||||
* @returns the least used color that is not the current color
|
||||
*/
|
||||
getNextAvailableColor(currentColor: string) {
|
||||
const colorUsageArray = this.colors.map(color => ({
|
||||
color,
|
||||
count: this.getColorUsageCount(color),
|
||||
}));
|
||||
const currentColorCount = this.getColorUsageCount(currentColor);
|
||||
const otherColors = colorUsageArray.filter(
|
||||
colorEntry => colorEntry.color !== currentColor,
|
||||
);
|
||||
// all other colors are used as much or more than currentColor
|
||||
const hasNoneAvailable = otherColors.every(
|
||||
colorEntry => colorEntry.count >= currentColorCount,
|
||||
getNextAvailableColor(currentLabel: string, currentColor: string): string {
|
||||
// Precompute color usage counts for all colors
|
||||
const colorUsageCounts = new Map(
|
||||
this.colors.map(color => [color, this.getColorUsageCount(color)]),
|
||||
);
|
||||
|
||||
// fallback to currentColor color
|
||||
if (!otherColors.length || hasNoneAvailable) {
|
||||
return currentColor;
|
||||
// Get an ordered array of labels from the map
|
||||
const orderedLabels = Array.from(this.chartLabelsColorMap.keys());
|
||||
const currentLabelIndex = orderedLabels.indexOf(currentLabel);
|
||||
|
||||
// Helper to infer "previous" and "next" labels based on index
|
||||
const getAdjacentLabelsColors = (): string[] => {
|
||||
const previousLabel =
|
||||
currentLabelIndex > 0 ? orderedLabels[currentLabelIndex - 1] : null;
|
||||
const nextLabel =
|
||||
currentLabelIndex < orderedLabels.length - 1
|
||||
? orderedLabels[currentLabelIndex + 1]
|
||||
: null;
|
||||
|
||||
const previousColor = previousLabel
|
||||
? this.chartLabelsColorMap.get(previousLabel)
|
||||
: null;
|
||||
const nextColor = nextLabel
|
||||
? this.chartLabelsColorMap.get(nextLabel)
|
||||
: null;
|
||||
|
||||
return [previousColor, nextColor].filter(color => color) as string[];
|
||||
};
|
||||
|
||||
const adjacentColors = getAdjacentLabelsColors();
|
||||
|
||||
// Determine adjusted score (usage count + penalties)
|
||||
const calculateScore = (color: string): number => {
|
||||
/* istanbul ignore next */
|
||||
const usageCount = colorUsageCounts.get(color) || 0;
|
||||
const adjacencyPenalty = adjacentColors.includes(color) ? 100 : 0;
|
||||
return usageCount + adjacencyPenalty;
|
||||
};
|
||||
|
||||
// If there is any color that has never been used, prioritize it
|
||||
const unusedColor = this.colors.find(
|
||||
color => (colorUsageCounts.get(color) || 0) === 0,
|
||||
);
|
||||
if (unusedColor) {
|
||||
return unusedColor;
|
||||
}
|
||||
|
||||
// Finding the least used color
|
||||
const leastUsedColor = otherColors.reduce((min, entry) =>
|
||||
entry.count < min.count ? entry : min,
|
||||
).color;
|
||||
// If all colors are used, calculate scores and choose the best one
|
||||
const otherColors = this.colors.filter(color => color !== currentColor);
|
||||
|
||||
return leastUsedColor;
|
||||
// Find the color with the minimum score, defaulting to currentColor
|
||||
return otherColors.reduce((bestColor, color) => {
|
||||
const bestScore = calculateScore(bestColor);
|
||||
const currentScore = calculateScore(color);
|
||||
return currentScore < bestScore ? color : bestColor;
|
||||
}, currentColor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import { makeSingleton } from '../utils';
|
||||
import CategoricalColorNamespace from './CategoricalColorNamespace';
|
||||
|
||||
export enum LabelsColorMapSource {
|
||||
Dashboard,
|
||||
@@ -25,7 +26,10 @@ export enum LabelsColorMapSource {
|
||||
}
|
||||
|
||||
export class LabelsColorMap {
|
||||
chartsLabelsMap: Map<number, { labels: string[]; scheme?: string }>;
|
||||
chartsLabelsMap: Map<
|
||||
number,
|
||||
{ labels: string[]; scheme?: string; ownScheme?: string }
|
||||
>;
|
||||
|
||||
colorMap: Map<string, string>;
|
||||
|
||||
@@ -38,17 +42,38 @@ export class LabelsColorMap {
|
||||
this.source = LabelsColorMapSource.Dashboard;
|
||||
}
|
||||
|
||||
updateColorMap(categoricalNamespace: any, colorScheme?: string) {
|
||||
const newColorMap = new Map();
|
||||
this.colorMap.clear();
|
||||
/**
|
||||
* Wipes out the color map and updates it with the new color scheme.
|
||||
*
|
||||
* @param categoricalNamespace - the namespace to use for color mapping
|
||||
* @param colorScheme - color scheme
|
||||
*/
|
||||
updateColorMap(
|
||||
categoricalNamespace: CategoricalColorNamespace,
|
||||
colorScheme?: string,
|
||||
merge = false,
|
||||
) {
|
||||
const newColorMap = this.colorMap;
|
||||
|
||||
if (!merge) {
|
||||
newColorMap.clear();
|
||||
}
|
||||
|
||||
this.chartsLabelsMap.forEach((chartConfig, sliceId) => {
|
||||
const { labels, scheme: originalChartColorScheme } = chartConfig;
|
||||
const currentColorScheme = colorScheme || originalChartColorScheme;
|
||||
const colorScale = categoricalNamespace.getScale(currentColorScheme);
|
||||
const { labels, ownScheme } = chartConfig;
|
||||
const appliedColorScheme = colorScheme || ownScheme;
|
||||
const colorScale = categoricalNamespace.getScale(appliedColorScheme);
|
||||
|
||||
labels.forEach(label => {
|
||||
const newColor = colorScale.getColor(label, sliceId);
|
||||
newColorMap.set(label, newColor);
|
||||
// if merge, apply the scheme only to new labels in the map
|
||||
if (!merge || !this.colorMap.has(label)) {
|
||||
const newColor = colorScale.getColor(
|
||||
label,
|
||||
sliceId,
|
||||
appliedColorScheme,
|
||||
);
|
||||
newColorMap.set(label, newColor);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.colorMap = newColorMap;
|
||||
@@ -58,29 +83,63 @@ export class LabelsColorMap {
|
||||
return this.colorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Called individually by each plugin via getColor fn.
|
||||
*
|
||||
* @param label - the label name
|
||||
* @param color - the color
|
||||
* @param sliceId - the chart id
|
||||
* @param colorScheme - the color scheme
|
||||
*
|
||||
*/
|
||||
addSlice(
|
||||
label: string,
|
||||
color: string,
|
||||
sliceId: number,
|
||||
colorScheme?: string,
|
||||
) {
|
||||
if (this.source !== LabelsColorMapSource.Dashboard) return;
|
||||
|
||||
const chartConfig = this.chartsLabelsMap.get(sliceId) || {
|
||||
labels: [],
|
||||
scheme: '',
|
||||
scheme: undefined,
|
||||
ownScheme: undefined,
|
||||
};
|
||||
|
||||
const { labels } = chartConfig;
|
||||
if (!labels.includes(label)) {
|
||||
labels.push(label);
|
||||
this.chartsLabelsMap.set(sliceId, {
|
||||
labels,
|
||||
scheme: colorScheme,
|
||||
ownScheme: chartConfig.ownScheme,
|
||||
});
|
||||
}
|
||||
this.colorMap.set(label, color);
|
||||
if (this.source === LabelsColorMapSource.Dashboard) {
|
||||
this.colorMap.set(label, color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to make sure all slices respect their original scheme.
|
||||
*
|
||||
* @param sliceId - the chart id
|
||||
* @param ownScheme - the color scheme
|
||||
*/
|
||||
setOwnColorScheme(sliceId: number, ownScheme: string) {
|
||||
const chartConfig = this.chartsLabelsMap.get(sliceId);
|
||||
if (chartConfig) {
|
||||
this.chartsLabelsMap.set(sliceId, {
|
||||
...chartConfig,
|
||||
ownScheme,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a slice from the color map.
|
||||
*
|
||||
* @param sliceId - the chart
|
||||
*/
|
||||
removeSlice(sliceId: number) {
|
||||
if (this.source !== LabelsColorMapSource.Dashboard) return;
|
||||
|
||||
@@ -96,10 +155,20 @@ export class LabelsColorMap {
|
||||
this.colorMap = newColorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the shared labels color map.
|
||||
*/
|
||||
clear() {
|
||||
this.chartsLabelsMap.clear();
|
||||
this.colorMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all maps
|
||||
*/
|
||||
reset() {
|
||||
this.clear();
|
||||
this.chartsLabelsMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const getInstance = makeSingleton(LabelsColorMap);
|
||||
|
||||
@@ -161,4 +161,63 @@ describe('CategoricalColorNamespace', () => {
|
||||
expect(color).toBe(color2);
|
||||
});
|
||||
});
|
||||
describe('statis resetColorsForLabels(labels)', () => {
|
||||
it('removes specified labels from forcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.setColor('label2', 'blue');
|
||||
namespace.resetColorsForLabels(['label1']);
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({ label2: 'blue' });
|
||||
});
|
||||
it('does not modify forcedItems if no labels are provided', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.setColor('label2', 'blue');
|
||||
namespace.resetColorsForLabels();
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({
|
||||
label1: 'red',
|
||||
label2: 'blue',
|
||||
});
|
||||
});
|
||||
it('does nothing if the label is not in forcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.resetColorsForLabels(['label2']); // label2 doesn't exist
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({ label1: 'red' });
|
||||
});
|
||||
it('removes all labels when all are provided', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.setColor('label2', 'blue');
|
||||
namespace.resetColorsForLabels(['label1', 'label2']);
|
||||
|
||||
expect(namespace.forcedItems).toMatchObject({});
|
||||
});
|
||||
it('creates a deep copy of forcedItems before modifying', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
|
||||
const originalForcedItems = namespace.forcedItems;
|
||||
namespace.resetColorsForLabels(['label1']);
|
||||
|
||||
expect(originalForcedItems).not.toBe(namespace.forcedItems);
|
||||
});
|
||||
it('removes the label if it exists in updatedForcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.resetColorsForLabels(['label1']);
|
||||
|
||||
expect(namespace.forcedItems).toEqual({});
|
||||
});
|
||||
it('does nothing for a label not in updatedForcedItems', () => {
|
||||
const namespace = getNamespace('test-reset-individual');
|
||||
namespace.setColor('label1', 'red');
|
||||
namespace.resetColorsForLabels(['label2']); // label2 doesn't exist
|
||||
|
||||
expect(namespace.forcedItems).toEqual({ label1: 'red' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
*/
|
||||
|
||||
import { ScaleOrdinal } from 'd3-scale';
|
||||
import { CategoricalColorScale, FeatureFlag } from '@superset-ui/core';
|
||||
import {
|
||||
CategoricalColorScale,
|
||||
FeatureFlag,
|
||||
LabelsColorMapSource,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
describe('CategoricalColorScale', () => {
|
||||
beforeEach(() => {
|
||||
@@ -43,7 +47,6 @@ describe('CategoricalColorScale', () => {
|
||||
expect(scale).toBeInstanceOf(CategoricalColorScale);
|
||||
expect(scale.forcedColors).toBe(forcedColors);
|
||||
});
|
||||
|
||||
it('can refer to colors based on their index', () => {
|
||||
const forcedColors = { pig: 1, horse: 5 };
|
||||
const scale = new CategoricalColorScale(
|
||||
@@ -67,7 +70,7 @@ describe('CategoricalColorScale', () => {
|
||||
>;
|
||||
let getNextAvailableColorSpy: jest.SpyInstance<
|
||||
string,
|
||||
[currentColor: string]
|
||||
[currentLabel: string, currentColor: string]
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -83,6 +86,36 @@ describe('CategoricalColorScale', () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('uses labelsColorMapInstance color map when source is Dashboard, otherwise uses chartLabelsColorMap', () => {
|
||||
const sliceId = 123;
|
||||
const colorScheme = 'preset';
|
||||
|
||||
// Mock chartLabelsColorMap and labelsColorMapInstance's getColorMap
|
||||
const chartColorMap = new Map([['testValueChart', 'chartColor']]);
|
||||
const dashboardColorMap = new Map([['testValueDash', 'dashboardColor']]);
|
||||
scale.chartLabelsColorMap = chartColorMap;
|
||||
jest
|
||||
.spyOn(scale.labelsColorMapInstance, 'getColorMap')
|
||||
.mockReturnValue(dashboardColorMap);
|
||||
|
||||
// Test when source is Dashboard
|
||||
scale.labelsColorMapInstance.source = LabelsColorMapSource.Dashboard;
|
||||
const colorFromDashboard = scale.getColor(
|
||||
'testValueDash',
|
||||
sliceId,
|
||||
colorScheme,
|
||||
);
|
||||
expect(colorFromDashboard).toBe('dashboardColor');
|
||||
|
||||
// Test when source is not Dashboard
|
||||
scale.labelsColorMapInstance.source = LabelsColorMapSource.Explore;
|
||||
const colorFromChart = scale.getColor(
|
||||
'testValueChart',
|
||||
sliceId,
|
||||
colorScheme,
|
||||
);
|
||||
expect(colorFromChart).toBe('chartColor');
|
||||
});
|
||||
it('returns same color for same value', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green'], {
|
||||
pig: 'red',
|
||||
@@ -177,7 +210,10 @@ describe('CategoricalColorScale', () => {
|
||||
scale.getColor('testValue3');
|
||||
scale.getColor('testValue4');
|
||||
|
||||
expect(getNextAvailableColorSpy).toHaveBeenCalledWith('blue');
|
||||
expect(getNextAvailableColorSpy).toHaveBeenCalledWith(
|
||||
'testValue4',
|
||||
'blue',
|
||||
);
|
||||
|
||||
getNextAvailableColorSpy.mockClear();
|
||||
|
||||
@@ -289,23 +325,25 @@ describe('CategoricalColorScale', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getNextAvailableColor(currentColor)', () => {
|
||||
describe('.getNextAvailableColor(currentLabel, currentColor)', () => {
|
||||
it('returns the current color if it is the least used or equally used among colors', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat');
|
||||
scale.getColor('dog');
|
||||
|
||||
// Since 'green' hasn't been used, it's considered the least used.
|
||||
expect(scale.getNextAvailableColor('blue')).toBe('green');
|
||||
expect(scale.getNextAvailableColor('fish', 'blue')).toBe('green');
|
||||
});
|
||||
|
||||
it('handles cases where all colors are equally used and returns the current color', () => {
|
||||
it('returns the least used color among all', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
scale.getColor('fish'); // green
|
||||
// All colors used once, so the function should return the current color
|
||||
expect(scale.getNextAvailableColor('red')).toBe('red');
|
||||
scale.getColor('puppy'); // blue
|
||||
scale.getColor('teddy'); // red
|
||||
// All colors used, so the function should return least used
|
||||
expect(scale.getNextAvailableColor('darling', 'red')).toBe('green');
|
||||
});
|
||||
|
||||
it('returns the least used color accurately even when some colors are used more frequently', () => {
|
||||
@@ -324,7 +362,57 @@ describe('CategoricalColorScale', () => {
|
||||
scale.getColor('pony'); // green
|
||||
|
||||
// Yellow is the least used color, so it should be returned.
|
||||
expect(scale.getNextAvailableColor('blue')).toBe('yellow');
|
||||
expect(scale.getNextAvailableColor('pony', 'blue')).toBe('yellow');
|
||||
});
|
||||
it('does not return adjacent colors if a non-adjacent color is equally used', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.chartLabelsColorMap.set('label1', 'red'); // Adjacent
|
||||
scale.chartLabelsColorMap.set('label2', 'blue'); // currentLabel
|
||||
scale.chartLabelsColorMap.set('label3', 'green'); // Adjacent
|
||||
|
||||
// Green and blue are equally used, but green is adjacent and penalized.
|
||||
expect(scale.getNextAvailableColor('label2', 'blue')).toBe('blue');
|
||||
});
|
||||
it('prioritizes a color that has never been used, even if there are adjacent colors', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
|
||||
scale.chartLabelsColorMap.set('label1', 'red');
|
||||
scale.chartLabelsColorMap.set('label2', 'blue'); // currentLabel
|
||||
|
||||
// Green has never been used, so it is prioritized.
|
||||
expect(scale.getNextAvailableColor('label2', 'blue')).toBe('green');
|
||||
});
|
||||
it('returns the least used or unused color when there are no adjacent labels', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
|
||||
// No adjacent labels are defined in chartLabelsColorMap.
|
||||
expect(scale.getNextAvailableColor('label2', 'green')).toBe('green');
|
||||
});
|
||||
it('handles colors that have never been used (fallback to usage count 0)', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
|
||||
// Do not use "green" at all
|
||||
scale.getColor('cat'); // blue
|
||||
scale.getColor('dog'); // red
|
||||
|
||||
// "green" has never been used, so usageCount for "green" should fallback to 0
|
||||
expect(scale.getNextAvailableColor('label2', 'red')).toBe('green');
|
||||
});
|
||||
it('handles a color with an explicit usage count of 0', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
|
||||
// Mock or override getColorUsageCount to return 0 for "blue"
|
||||
jest.spyOn(scale, 'getColorUsageCount').mockImplementation(color => {
|
||||
if (color === 'blue') return 0; // Explicitly return 0 for "blue"
|
||||
return 1; // Return 1 for other colors
|
||||
});
|
||||
|
||||
// "blue" should still be a valid option with a usage count of 0
|
||||
expect(scale.getNextAvailableColor('label1', 'red')).toBe('blue');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('LabelsColorMap', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
getLabelsColorMap().source = LabelsColorMapSource.Dashboard;
|
||||
getLabelsColorMap().clear();
|
||||
getLabelsColorMap().reset();
|
||||
});
|
||||
|
||||
it('has default value out-of-the-box', () => {
|
||||
@@ -92,11 +92,17 @@ describe('LabelsColorMap', () => {
|
||||
expect(Object.fromEntries(colorMap)).toEqual({ b: 'green' });
|
||||
});
|
||||
|
||||
it('should do nothing when source is not dashboard', () => {
|
||||
it('should set a new color only when source is dashboard', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Explore;
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
expect(Object.fromEntries(labelsColorMap.chartsLabelsMap)).toEqual({});
|
||||
const colorMap = labelsColorMap.getColorMap();
|
||||
expect(Object.fromEntries(colorMap)).toEqual({});
|
||||
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
const colorMap2 = labelsColorMap.getColorMap();
|
||||
expect(Object.fromEntries(colorMap2)).toEqual({ a: 'red' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,7 +132,7 @@ describe('LabelsColorMap', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.updateColorMap(namespace, scheme)', () => {
|
||||
describe('.updateColorMap(namespace, scheme, merge)', () => {
|
||||
let categoricalNamespace: any;
|
||||
let mockedNamespace: any;
|
||||
let labelsColorMap: any;
|
||||
@@ -141,18 +147,24 @@ describe('LabelsColorMap', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('should clear color map when not merge', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2', false);
|
||||
expect(labelsColorMap.colorMap).toEqual(new Map([['a', 'mockColor']]));
|
||||
});
|
||||
|
||||
it('should not clear color map when merge', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2', true);
|
||||
expect(labelsColorMap.colorMap).not.toEqual(new Map());
|
||||
});
|
||||
|
||||
it('should use provided color scheme', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2');
|
||||
expect(mockedNamespace.getScale).toHaveBeenCalledWith('testColors2');
|
||||
});
|
||||
|
||||
it('should fallback to original chart color scheme if no color scheme is provided', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1, 'originalScheme');
|
||||
labelsColorMap.updateColorMap(mockedNamespace);
|
||||
expect(mockedNamespace.getScale).toHaveBeenCalledWith('originalScheme');
|
||||
});
|
||||
|
||||
it('should fallback to undefined if no color scheme is provided', () => {
|
||||
labelsColorMap.addSlice('a', 'red', 1);
|
||||
labelsColorMap.addSlice('b', 'blue', 2);
|
||||
@@ -181,6 +193,23 @@ describe('LabelsColorMap', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should update only new labels in the color map when merge', () => {
|
||||
labelsColorMap.colorMap = new Map();
|
||||
|
||||
labelsColorMap.addSlice('a', 'yellow', 1);
|
||||
labelsColorMap.addSlice('b', 'green', 1);
|
||||
labelsColorMap.addSlice('c', 'purple', 1);
|
||||
|
||||
labelsColorMap.updateColorMap(categoricalNamespace, 'testColors2', true);
|
||||
|
||||
const mergedColorMap = labelsColorMap.getColorMap();
|
||||
expect(Object.fromEntries(mergedColorMap)).toEqual({
|
||||
a: 'yellow',
|
||||
b: 'green',
|
||||
c: 'purple',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use recycle colors', () => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.UseAnalagousColors]: false,
|
||||
@@ -231,4 +260,47 @@ describe('LabelsColorMap', () => {
|
||||
expect(Object.fromEntries(colorMap)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOwnColorScheme(sliceId, ownScheme)', () => {
|
||||
it('should update the scheme in the config', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Explore;
|
||||
const sliceId = 1;
|
||||
const initialConfig = { labels: ['initial config'] };
|
||||
|
||||
labelsColorMap.chartsLabelsMap = new Map();
|
||||
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
|
||||
|
||||
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
|
||||
|
||||
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
|
||||
...initialConfig,
|
||||
ownScheme: 'newScheme',
|
||||
});
|
||||
});
|
||||
it('should update ownScheme when source is not Explore', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
const sliceId = 1;
|
||||
const initialConfig = { labels: ['initial config'] };
|
||||
|
||||
labelsColorMap.chartsLabelsMap = new Map();
|
||||
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
|
||||
|
||||
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
|
||||
|
||||
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
|
||||
...initialConfig,
|
||||
ownScheme: 'newScheme',
|
||||
});
|
||||
});
|
||||
it('should do nothing when chart config does not exist', () => {
|
||||
const labelsColorMap = getLabelsColorMap();
|
||||
labelsColorMap.source = LabelsColorMapSource.Explore;
|
||||
const sliceId = 1;
|
||||
|
||||
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
|
||||
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ function Chord(element, props) {
|
||||
.append('path')
|
||||
.attr('id', (d, i) => `group${i}`)
|
||||
.attr('d', arc)
|
||||
.style('fill', (d, i) => colorFn(nodes[i], sliceId, colorScheme));
|
||||
.style('fill', (d, i) => colorFn(nodes[i], sliceId));
|
||||
|
||||
// Add a text label.
|
||||
const groupText = group.append('text').attr('x', 6).attr('dy', 15);
|
||||
@@ -121,7 +121,7 @@ function Chord(element, props) {
|
||||
.on('mouseover', d => {
|
||||
chord.classed('fade', p => p !== d);
|
||||
})
|
||||
.style('fill', d => colorFn(nodes[d.source.index], sliceId, colorScheme))
|
||||
.style('fill', d => colorFn(nodes[d.source.index], sliceId))
|
||||
.attr('d', path);
|
||||
|
||||
// Add an elaborate mouseover title for each chord.
|
||||
|
||||
@@ -37,6 +37,7 @@ const propTypes = {
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
country: PropTypes.string,
|
||||
colorScheme: PropTypes.string,
|
||||
linearColorScheme: PropTypes.string,
|
||||
mapBaseUrl: PropTypes.string,
|
||||
numberFormat: PropTypes.string,
|
||||
|
||||
@@ -73,12 +73,11 @@ class CustomHistogram extends PureComponent {
|
||||
showLegend,
|
||||
sliceId,
|
||||
} = this.props;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const keys = data.map(d => d.key);
|
||||
const colorScale = scaleOrdinal({
|
||||
domain: keys,
|
||||
range: keys.map(x => colorFn(x, sliceId, colorScheme)),
|
||||
range: keys.map(x => colorFn(x, sliceId)),
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -384,7 +384,7 @@ function Icicle(element, props) {
|
||||
|
||||
// Apply color scheme
|
||||
g.selectAll('rect').style('fill', d => {
|
||||
d.color = colorFn(d.name, sliceId, colorScheme);
|
||||
d.color = colorFn(d.name, sliceId);
|
||||
|
||||
return d.color;
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ const propTypes = {
|
||||
numberFormat: PropTypes.string,
|
||||
useRichTooltip: PropTypes.bool,
|
||||
useAreaProportions: PropTypes.bool,
|
||||
colorScheme: PropTypes.string,
|
||||
};
|
||||
|
||||
function copyArc(d) {
|
||||
@@ -120,14 +121,14 @@ function Rose(element, props) {
|
||||
.map(v => ({
|
||||
key: v.name,
|
||||
value: v.value,
|
||||
color: colorFn(v.name, sliceId, colorScheme),
|
||||
color: colorFn(v.name, sliceId),
|
||||
highlight: v.id === d.arcId,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: d.name,
|
||||
value: d.val,
|
||||
color: colorFn(d.name, sliceId, colorScheme),
|
||||
color: colorFn(d.name, sliceId),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -138,7 +139,7 @@ function Rose(element, props) {
|
||||
};
|
||||
}
|
||||
|
||||
legend.width(width).color(d => colorFn(d.key, sliceId, colorScheme));
|
||||
legend.width(width).color(d => colorFn(d.key, sliceId));
|
||||
legendWrap.datum(legendData(datum)).call(legend);
|
||||
|
||||
tooltip.headerFormatter(timeFormat).valueFormatter(format);
|
||||
@@ -385,7 +386,7 @@ function Rose(element, props) {
|
||||
const arcs = ae
|
||||
.append('path')
|
||||
.attr('class', 'arc')
|
||||
.attr('fill', d => colorFn(d.name, sliceId, colorScheme))
|
||||
.attr('fill', d => colorFn(d.name, sliceId))
|
||||
.attr('d', arc);
|
||||
|
||||
function mousemove() {
|
||||
|
||||
@@ -83,7 +83,7 @@ function computeGraph(links) {
|
||||
|
||||
function SankeyLoop(element, props) {
|
||||
const { data, width, height, colorScheme, sliceId } = props;
|
||||
const color = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const margin = { ...defaultMargin, ...props.margin };
|
||||
const innerWidth = width - margin.left - margin.right;
|
||||
const innerHeight = height - margin.top - margin.bottom;
|
||||
@@ -107,7 +107,7 @@ function SankeyLoop(element, props) {
|
||||
value / sValue,
|
||||
)})`,
|
||||
)
|
||||
.linkColor(d => color(d.source.name, sliceId));
|
||||
.linkColor(d => colorFn(d.source.name, sliceId));
|
||||
|
||||
const div = select(element);
|
||||
div.selectAll('*').remove();
|
||||
|
||||
@@ -67,7 +67,6 @@ function Sankey(element, props) {
|
||||
.attr('height', innerHeight + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
const sankey = d3Sankey()
|
||||
@@ -219,7 +218,7 @@ function Sankey(element, props) {
|
||||
.attr('width', sankey.nodeWidth())
|
||||
.style('fill', d => {
|
||||
const name = d.name || 'N/A';
|
||||
d.color = colorFn(name, sliceId, colorScheme);
|
||||
d.color = colorFn(name, sliceId);
|
||||
|
||||
return d.color;
|
||||
})
|
||||
|
||||
@@ -43,6 +43,7 @@ const propTypes = {
|
||||
showBubbles: PropTypes.bool,
|
||||
linearColorScheme: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
colorScheme: PropTypes.string,
|
||||
setDataMask: PropTypes.func,
|
||||
onContextMenu: PropTypes.func,
|
||||
emitCrossFilters: PropTypes.bool,
|
||||
@@ -85,24 +86,24 @@ function WorldMap(element, props) {
|
||||
.range([1, maxBubbleSize]);
|
||||
|
||||
let processedData;
|
||||
let colorScale;
|
||||
let colorFn;
|
||||
if (colorBy === ColorBy.Country) {
|
||||
colorScale = CategoricalColorNamespace.getScale(colorScheme);
|
||||
colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
processedData = filteredData.map(d => ({
|
||||
...d,
|
||||
radius: radiusScale(Math.sqrt(d.m2)),
|
||||
fillColor: colorScale(d.name, sliceId),
|
||||
fillColor: colorFn(d.name, sliceId),
|
||||
}));
|
||||
} else {
|
||||
colorScale = getSequentialSchemeRegistry()
|
||||
colorFn = getSequentialSchemeRegistry()
|
||||
.get(linearColorScheme)
|
||||
.createLinearScale(d3Extent(filteredData, d => d.m1));
|
||||
|
||||
processedData = filteredData.map(d => ({
|
||||
...d,
|
||||
radius: radiusScale(Math.sqrt(d.m2)),
|
||||
fillColor: colorScale(d.m1),
|
||||
fillColor: colorFn(d.m1),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -52,16 +52,14 @@ const { getScale } = CategoricalColorNamespace;
|
||||
function getCategories(fd: QueryFormData, data: JsonObject[]) {
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||
const colorFn = getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorFn = getScale(appliedScheme);
|
||||
const categories = {};
|
||||
data.forEach(d => {
|
||||
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
color = hexToRGB(
|
||||
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
|
||||
c.a * 255,
|
||||
);
|
||||
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
|
||||
} else {
|
||||
color = fixedColor;
|
||||
}
|
||||
@@ -132,15 +130,13 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
|
||||
const addColor = useCallback((data: JsonObject[], fd: QueryFormData) => {
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const colorFn = getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorFn = getScale(appliedScheme);
|
||||
|
||||
return data.map(d => {
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
color = hexToRGB(
|
||||
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
|
||||
c.a * 255,
|
||||
);
|
||||
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
|
||||
|
||||
return { ...d, color };
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ export function getLayer(
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
const fd = formData;
|
||||
const colorScale = CategoricalColorNamespace.getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
const colorRange = colorScale
|
||||
.range()
|
||||
.map(color => hexToRGB(color)) as Color[];
|
||||
|
||||
@@ -54,7 +54,8 @@ export function getLayer(
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
const fd = formData;
|
||||
const colorScale = CategoricalColorNamespace.getScale(fd.color_scheme);
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
const colorRange = colorScale
|
||||
.range()
|
||||
.map(color => hexToRGB(color)) as Color[];
|
||||
|
||||
@@ -658,9 +658,7 @@ function nvd3Vis(element, props) {
|
||||
} else if (vizType !== 'bullet') {
|
||||
const colorFn = getScale(colorScheme);
|
||||
chart.color(
|
||||
d =>
|
||||
d.color ||
|
||||
colorFn(cleanColorInput(d[colorKey]), sliceId, colorScheme),
|
||||
d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,9 +109,9 @@ export default function transformProps(
|
||||
datum[`${metric}__outliers`],
|
||||
],
|
||||
itemStyle: {
|
||||
color: colorFn(groupbyLabel, sliceId, colorScheme),
|
||||
color: colorFn(groupbyLabel, sliceId),
|
||||
opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6,
|
||||
borderColor: colorFn(groupbyLabel, sliceId, colorScheme),
|
||||
borderColor: colorFn(groupbyLabel, sliceId),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -150,7 +150,7 @@ export default function transformProps(
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
color: colorFn(groupbyLabel, sliceId, colorScheme),
|
||||
color: colorFn(groupbyLabel, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -108,8 +108,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
|
||||
legendOrientation,
|
||||
legendMargin,
|
||||
legendType,
|
||||
sliceId,
|
||||
}: EchartsBubbleFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
|
||||
const legends = new Set<string>();
|
||||
@@ -138,7 +138,10 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
|
||||
],
|
||||
],
|
||||
type: 'scatter',
|
||||
itemStyle: { color: colorFn(name), opacity },
|
||||
itemStyle: {
|
||||
color: colorFn(name, sliceId),
|
||||
opacity,
|
||||
},
|
||||
});
|
||||
legends.add(name);
|
||||
});
|
||||
|
||||
@@ -145,7 +145,6 @@ export default function transformProps(
|
||||
}, {});
|
||||
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getValueFormatter(
|
||||
metric,
|
||||
@@ -175,7 +174,7 @@ export default function transformProps(
|
||||
value,
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(name, sliceId, colorScheme),
|
||||
color: colorFn(name, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -166,6 +166,7 @@ export default function transformProps(
|
||||
const name = groupbyLabels
|
||||
.map(column => `${verboseMap[column] || column}: ${data_point[column]}`)
|
||||
.join(', ');
|
||||
const colorLabel = groupbyLabels.map(col => data_point[col] as string);
|
||||
columnsLabelMap.set(
|
||||
name,
|
||||
groupbyLabels.map(col => data_point[col] as string),
|
||||
@@ -174,7 +175,7 @@ export default function transformProps(
|
||||
value: data_point[metricLabel] as number,
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(index, sliceId, colorScheme),
|
||||
color: colorFn(colorLabel, sliceId),
|
||||
},
|
||||
title: {
|
||||
offsetCenter: [
|
||||
@@ -202,7 +203,7 @@ export default function transformProps(
|
||||
item = {
|
||||
...item,
|
||||
itemStyle: {
|
||||
color: colorFn(index, sliceId, colorScheme),
|
||||
color: colorFn(index, sliceId),
|
||||
opacity: OpacityEnum.SemiTransparent,
|
||||
},
|
||||
detail: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -191,7 +191,6 @@ export default function transformProps(
|
||||
}, {});
|
||||
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getValueFormatter(
|
||||
metric,
|
||||
@@ -223,7 +222,7 @@ export default function transformProps(
|
||||
value,
|
||||
name,
|
||||
itemStyle: {
|
||||
color: colorFn(name, sliceId, colorScheme),
|
||||
color: colorFn(name, sliceId),
|
||||
opacity: isFiltered
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
|
||||
@@ -109,7 +109,6 @@ export default function transformProps(
|
||||
...formData,
|
||||
};
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getNumberFormatter(numberFormat);
|
||||
const formatter = (params: CallbackDataParams) =>
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
SupersetThemeProps,
|
||||
withTheme,
|
||||
seed,
|
||||
CategoricalColorScale,
|
||||
CategoricalColorNamespace,
|
||||
} from '@superset-ui/core';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@@ -230,7 +230,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
encoder.channels.color.setDomainFromDataset(words);
|
||||
|
||||
const { getValueFromDatum } = encoder.channels.color;
|
||||
const colorFn = encoder.channels.color.scale as CategoricalColorScale;
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
const viewBoxWidth = width * scaleFactor;
|
||||
const viewBoxHeight = height * scaleFactor;
|
||||
@@ -250,11 +250,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
fontSize={`${w.size}px`}
|
||||
fontWeight={w.weight}
|
||||
fontFamily={w.font}
|
||||
fill={colorFn(
|
||||
getValueFromDatum(w) as string,
|
||||
sliceId,
|
||||
colorScheme,
|
||||
)}
|
||||
fill={colorFn(getValueFromDatum(w) as string, sliceId)}
|
||||
textAnchor="middle"
|
||||
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
|
||||
>
|
||||
|
||||
@@ -26,10 +26,7 @@ export const getDashboardFormData = (overrides: JsonObject = {}) => ({
|
||||
girl: '#FF69B4',
|
||||
boy: '#ADD8E6',
|
||||
},
|
||||
shared_label_colors: {
|
||||
boy: '#ADD8E6',
|
||||
girl: '#FF69B4',
|
||||
},
|
||||
shared_label_colors: ['boy', 'girl'],
|
||||
color_scheme: 'd3Category20b',
|
||||
extra_filters: [
|
||||
{
|
||||
|
||||
@@ -113,6 +113,6 @@ export const overwriteConfirmMetadata = {
|
||||
slug: null,
|
||||
owners: [],
|
||||
json_metadata:
|
||||
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":{"Male":"#5ac19e","Female":"#1f86c9","<NULL>":"#5AC189","Prefer not to say":"#47457c","No Answer":"#e05043","Yes, an ethnic minority":"#666666","No, not an ethnic minority":"#ffa444","age":"#1FA8C9"},"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
|
||||
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":["Male", "Female","<NULL>","Prefer not to say","No Answer","Yes, an ethnic minority","No, not an ethnic minority","age"],"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 }) => `
|
||||
|
||||
39
superset-frontend/src/components/MetadataBar/constants.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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" />;
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import { Input } from 'src/components/Input';
|
||||
import { FormItem } from 'src/components/Form';
|
||||
@@ -44,9 +44,19 @@ import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import TagType from 'src/types/TagType';
|
||||
import { fetchTags, OBJECT_TYPES } from 'src/features/tags/tags';
|
||||
import { loadTags } from 'src/components/Tags/utils';
|
||||
import { applyColors, getColorNamespace } from 'src/utils/colorScheme';
|
||||
import {
|
||||
applyColors,
|
||||
getColorNamespace,
|
||||
getLabelsColorMapEntries,
|
||||
} from 'src/utils/colorScheme';
|
||||
import getOwnerName from 'src/utils/getOwnerName';
|
||||
import Owner from 'src/types/Owner';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
setColorScheme,
|
||||
setDashboardMetadata,
|
||||
} from 'src/dashboard/actions/dashboardState';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
|
||||
const StyledFormItem = styled(FormItem)`
|
||||
margin-bottom: 0;
|
||||
@@ -84,6 +94,7 @@ type DashboardInfo = {
|
||||
certifiedBy: string;
|
||||
certificationDetails: string;
|
||||
isManagedExternally: boolean;
|
||||
metadata: Record<string, any>;
|
||||
};
|
||||
|
||||
const PropertiesModal = ({
|
||||
@@ -98,10 +109,11 @@ const PropertiesModal = ({
|
||||
onSubmit = () => {},
|
||||
show = false,
|
||||
}: PropertiesModalProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const [form] = AntdForm.useForm();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
|
||||
const [colorScheme, setColorScheme] = useState(currentColorScheme);
|
||||
const [colorScheme, setCurrentColorScheme] = useState(currentColorScheme);
|
||||
const [jsonMetadata, setJsonMetadata] = useState('');
|
||||
const [dashboardInfo, setDashboardInfo] = useState<DashboardInfo>();
|
||||
const [owners, setOwners] = useState<Owners>([]);
|
||||
@@ -109,6 +121,7 @@ const PropertiesModal = ({
|
||||
const saveLabel = onlyApply ? t('Apply') : t('Save');
|
||||
const [tags, setTags] = useState<TagType[]>([]);
|
||||
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
|
||||
const originalDashboardMetadata = useRef<Record<string, any>>({});
|
||||
|
||||
const tagsAsSelectValues = useMemo(() => {
|
||||
const selectTags = tags.map((tag: { id: number; name: string }) => ({
|
||||
@@ -182,21 +195,24 @@ const PropertiesModal = ({
|
||||
certifiedBy: certified_by || '',
|
||||
certificationDetails: certification_details || '',
|
||||
isManagedExternally: is_managed_externally || false,
|
||||
metadata,
|
||||
};
|
||||
|
||||
form.setFieldsValue(dashboardInfo);
|
||||
setDashboardInfo(dashboardInfo);
|
||||
setOwners(owners);
|
||||
setRoles(roles);
|
||||
setColorScheme(metadata.color_scheme);
|
||||
setCurrentColorScheme(metadata.color_scheme);
|
||||
|
||||
const metaDataCopy = omit(metadata, [
|
||||
'positions',
|
||||
'shared_label_colors',
|
||||
'map_label_colors',
|
||||
'color_scheme_domain',
|
||||
]);
|
||||
|
||||
setJsonMetadata(metaDataCopy ? jsonStringify(metaDataCopy) : '');
|
||||
originalDashboardMetadata.current = metadata;
|
||||
},
|
||||
[form],
|
||||
);
|
||||
@@ -269,6 +285,8 @@ const PropertiesModal = ({
|
||||
return parsedRoles;
|
||||
};
|
||||
|
||||
const handleOnCancel = () => onHide();
|
||||
|
||||
const onColorSchemeChange = (
|
||||
colorScheme = '',
|
||||
{ updateMetadata = true } = {},
|
||||
@@ -287,20 +305,21 @@ const PropertiesModal = ({
|
||||
throw new Error('A valid color scheme is required');
|
||||
}
|
||||
|
||||
jsonMetadataObj.color_scheme = colorScheme;
|
||||
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
|
||||
|
||||
setCurrentColorScheme(colorScheme);
|
||||
dispatch(setColorScheme(colorScheme));
|
||||
|
||||
// update metadata to match selection
|
||||
if (updateMetadata) {
|
||||
jsonMetadataObj.color_scheme = colorScheme;
|
||||
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
|
||||
|
||||
setJsonMetadata(jsonStringify(jsonMetadataObj));
|
||||
}
|
||||
setColorScheme(colorScheme);
|
||||
};
|
||||
|
||||
const onFinish = () => {
|
||||
const { title, slug, certifiedBy, certificationDetails } =
|
||||
form.getFieldsValue();
|
||||
let currentColorScheme = colorScheme;
|
||||
let currentJsonMetadata = jsonMetadata;
|
||||
|
||||
// validate currentJsonMetadata
|
||||
@@ -318,29 +337,48 @@ const PropertiesModal = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const copyMetadata = { ...metadata };
|
||||
const colorNamespace = getColorNamespace(metadata?.color_namespace);
|
||||
|
||||
// color scheme in json metadata has precedence over selection
|
||||
currentColorScheme = metadata?.color_scheme || colorScheme;
|
||||
const updatedColorScheme = metadata?.color_scheme || colorScheme;
|
||||
const shouldGoFresh =
|
||||
updatedColorScheme !== originalDashboardMetadata.current.color_scheme;
|
||||
const shouldResetCustomLabels = !areObjectsEqual(
|
||||
originalDashboardMetadata.current.label_colors || {},
|
||||
metadata?.label_colors || {},
|
||||
);
|
||||
const currentCustomLabels = Object.keys(metadata?.label_colors || {});
|
||||
const prevCustomLabels = Object.keys(
|
||||
originalDashboardMetadata.current.label_colors || {},
|
||||
);
|
||||
const resettableCustomLabels =
|
||||
currentCustomLabels.length > 0 ? currentCustomLabels : prevCustomLabels;
|
||||
const freshCustomLabels =
|
||||
shouldResetCustomLabels && resettableCustomLabels.length > 0
|
||||
? resettableCustomLabels
|
||||
: false;
|
||||
const jsonMetadataObj = getJsonMetadata();
|
||||
const customLabelColors = jsonMetadataObj.label_colors || {};
|
||||
const updatedDashboardMetadata = {
|
||||
...originalDashboardMetadata.current,
|
||||
label_colors: customLabelColors,
|
||||
color_scheme: updatedColorScheme,
|
||||
};
|
||||
|
||||
// remove information from user facing input
|
||||
if (metadata?.shared_label_colors) {
|
||||
delete metadata.shared_label_colors;
|
||||
}
|
||||
if (metadata?.color_scheme_domain) {
|
||||
delete metadata.color_scheme_domain;
|
||||
}
|
||||
originalDashboardMetadata.current = updatedDashboardMetadata;
|
||||
applyColors(updatedDashboardMetadata, shouldGoFresh || freshCustomLabels);
|
||||
dispatch(
|
||||
setDashboardMetadata({
|
||||
...updatedDashboardMetadata,
|
||||
map_label_colors: getLabelsColorMapEntries(customLabelColors),
|
||||
}),
|
||||
);
|
||||
|
||||
// only apply colors, the user has not saved yet
|
||||
applyColors(copyMetadata, true);
|
||||
|
||||
currentJsonMetadata = jsonStringify(metadata);
|
||||
|
||||
onColorSchemeChange(currentColorScheme, {
|
||||
onColorSchemeChange(updatedColorScheme, {
|
||||
updateMetadata: false,
|
||||
});
|
||||
|
||||
currentJsonMetadata = jsonStringify(metadata);
|
||||
|
||||
const moreOnSubmitProps: { roles?: Roles } = {};
|
||||
const morePutProps: { roles?: number[]; tags?: (number | undefined)[] } =
|
||||
{};
|
||||
@@ -557,14 +595,14 @@ const PropertiesModal = ({
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
onHide={handleOnCancel}
|
||||
title={t('Dashboard properties')}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
htmlType="button"
|
||||
buttonSize="small"
|
||||
onClick={onHide}
|
||||
onClick={handleOnCancel}
|
||||
data-test="properties-modal-cancel-button"
|
||||
cta
|
||||
>
|
||||
|
||||
@@ -66,8 +66,8 @@ const SyncDashboardState: FC<Props> = ({ dashboardPageId }) => {
|
||||
>(
|
||||
({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
|
||||
labelsColor: dashboardInfo.metadata?.label_colors || EMPTY_OBJECT,
|
||||
labelsColorMap:
|
||||
dashboardInfo.metadata?.shared_label_colors || EMPTY_OBJECT,
|
||||
labelsColorMap: dashboardInfo.metadata?.map_label_colors || EMPTY_OBJECT,
|
||||
sharedLabelsColors: dashboardInfo.metadata?.shared_label_colors || [],
|
||||
colorScheme: dashboardState?.colorScheme,
|
||||
chartConfiguration:
|
||||
dashboardInfo.metadata?.chart_configuration || EMPTY_OBJECT,
|
||||
|
||||
@@ -197,11 +197,19 @@ class Chart extends Component {
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
// chart should re-render if color scheme or label color was changed
|
||||
// chart should re-render if color scheme or label colors were changed
|
||||
nextProps.formData?.color_scheme !== this.props.formData?.color_scheme ||
|
||||
!areObjectsEqual(
|
||||
nextProps.formData?.label_colors,
|
||||
this.props.formData?.label_colors,
|
||||
nextProps.formData?.label_colors || {},
|
||||
this.props.formData?.label_colors || {},
|
||||
) ||
|
||||
!areObjectsEqual(
|
||||
nextProps.formData?.map_label_colors || {},
|
||||
this.props.formData?.map_label_colors || {},
|
||||
) ||
|
||||
!isEqual(
|
||||
nextProps.formData?.shared_label_colors || [],
|
||||
this.props.formData?.shared_label_colors || [],
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
|
||||
@@ -33,7 +33,7 @@ export const crossFiltersSelector = (props: {
|
||||
verboseMaps: { [key: string]: Record<string, string> };
|
||||
}): CrossFilterIndicator[] => {
|
||||
const { dataMask, chartConfiguration, dashboardLayout, verboseMaps } = props;
|
||||
const chartsIds = Object.keys(chartConfiguration);
|
||||
const chartsIds = Object.keys(chartConfiguration || {});
|
||||
|
||||
return chartsIds
|
||||
.map(chartId => {
|
||||
|
||||
@@ -41,7 +41,8 @@ const initialState: { dashboardInfo: DashboardInfo } = {
|
||||
color_namespace: '',
|
||||
color_scheme_domain: [],
|
||||
label_colors: {},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
map_label_colors: {},
|
||||
cross_filters_enabled: false,
|
||||
},
|
||||
json_metadata: '',
|
||||
@@ -51,6 +52,9 @@ const initialState: { dashboardInfo: DashboardInfo } = {
|
||||
conf: {},
|
||||
},
|
||||
crossFiltersEnabled: true,
|
||||
created_on_delta_humanized: '',
|
||||
changed_on_delta_humanized: '',
|
||||
owners: [],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -485,7 +485,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
|
||||
Sports: '#8FD3E4',
|
||||
Strategy: '#A1A6BD',
|
||||
},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
color_scheme: 'supersetColors',
|
||||
extra_filters: [
|
||||
{
|
||||
@@ -710,7 +710,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
|
||||
queryFields: {
|
||||
metric: 'metrics',
|
||||
},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
color_scheme: 'supersetColors',
|
||||
extra_filters: [
|
||||
{
|
||||
@@ -759,7 +759,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
|
||||
queryFields: {
|
||||
metric: 'metrics',
|
||||
},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
color_scheme: 'supersetColors',
|
||||
dashboardId: 9,
|
||||
applied_time_extras: {},
|
||||
@@ -3046,7 +3046,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
|
||||
groupby: 'groupby',
|
||||
metrics: 'metrics',
|
||||
},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
extra_filters: [
|
||||
{
|
||||
col: '__time_range',
|
||||
@@ -3125,7 +3125,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
|
||||
groupby: 'groupby',
|
||||
metrics: 'metrics',
|
||||
},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
dashboardId: 9,
|
||||
applied_time_extras: {},
|
||||
where: '',
|
||||
@@ -16514,7 +16514,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
|
||||
groupby: 'groupby',
|
||||
metrics: 'metrics',
|
||||
},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
extra_filters: [
|
||||
{
|
||||
col: '__time_range',
|
||||
@@ -16661,7 +16661,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
|
||||
groupby: 'groupby',
|
||||
metrics: 'metrics',
|
||||
},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
dashboardId: 9,
|
||||
applied_time_extras: {},
|
||||
where: '',
|
||||
@@ -17523,7 +17523,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
|
||||
Sports: '#8FD3E4',
|
||||
Strategy: '#A1A6BD',
|
||||
},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
extra_filters: [
|
||||
{
|
||||
col: '__time_range',
|
||||
@@ -17678,7 +17678,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
|
||||
groupby: 'groupby',
|
||||
metrics: 'metrics',
|
||||
},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
extra_filters: [
|
||||
{
|
||||
col: '__time_range',
|
||||
@@ -17715,7 +17715,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
|
||||
groupby: 'groupby',
|
||||
metrics: 'metrics',
|
||||
},
|
||||
shared_label_colors: {},
|
||||
shared_label_colors: [],
|
||||
dashboardId: 9,
|
||||
applied_time_extras: {},
|
||||
where: '',
|
||||
|
||||
@@ -59,17 +59,24 @@ function mapStateToProps(
|
||||
const datasource =
|
||||
(chart && chart.form_data && datasources[chart.form_data.datasource]) ||
|
||||
PLACEHOLDER_DATASOURCE;
|
||||
const { colorScheme, colorNamespace, datasetsStatus } = dashboardState;
|
||||
const {
|
||||
colorScheme: appliedColorScheme,
|
||||
colorNamespace,
|
||||
datasetsStatus,
|
||||
} = dashboardState;
|
||||
const labelsColor = dashboardInfo?.metadata?.label_colors || {};
|
||||
const labelsColorMap = dashboardInfo?.metadata?.shared_label_colors || {};
|
||||
const labelsColorMap = dashboardInfo?.metadata?.map_label_colors || {};
|
||||
const sharedLabelsColors = dashboardInfo?.metadata?.shared_label_colors || [];
|
||||
const ownColorScheme = chart.form_data?.color_scheme;
|
||||
// note: this method caches filters if possible to prevent render cascades
|
||||
const formData = getFormDataWithExtraFilters({
|
||||
chart,
|
||||
chartConfiguration: dashboardInfo.metadata?.chart_configuration,
|
||||
charts: chartQueries,
|
||||
filters: getAppliedFilterValues(id),
|
||||
colorScheme,
|
||||
colorNamespace,
|
||||
colorScheme: appliedColorScheme,
|
||||
ownColorScheme,
|
||||
sliceId: id,
|
||||
nativeFilters: nativeFilters?.filters,
|
||||
allSliceIds: dashboardState.sliceIds,
|
||||
@@ -77,6 +84,7 @@ function mapStateToProps(
|
||||
extraControls,
|
||||
labelsColor,
|
||||
labelsColorMap,
|
||||
sharedLabelsColors,
|
||||
});
|
||||
|
||||
formData.dashboardId = dashboardInfo.id;
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { ChartConfiguration, ChartQueryPayload } from 'src/dashboard/types';
|
||||
import { getExtraFormData } from 'src/dashboard/components/nativeFilters/utils';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
import { isEqual } from 'lodash';
|
||||
import getEffectiveExtraFilters from './getEffectiveExtraFilters';
|
||||
import { getAllActiveFilters } from '../activeAllDashboardFilters';
|
||||
|
||||
@@ -38,6 +39,7 @@ export interface GetFormDataWithExtraFiltersArguments {
|
||||
chart: ChartQueryPayload;
|
||||
filters: DataRecordFilters;
|
||||
colorScheme?: string;
|
||||
ownColorScheme?: string;
|
||||
colorNamespace?: string;
|
||||
sliceId: number;
|
||||
dataMask: DataMaskStateWithId;
|
||||
@@ -45,6 +47,7 @@ export interface GetFormDataWithExtraFiltersArguments {
|
||||
extraControls: Record<string, string | boolean | null>;
|
||||
labelsColor?: Record<string, string>;
|
||||
labelsColorMap?: Record<string, string>;
|
||||
sharedLabelsColors?: string[];
|
||||
allSliceIds: number[];
|
||||
}
|
||||
|
||||
@@ -57,30 +60,32 @@ export default function getFormDataWithExtraFilters({
|
||||
nativeFilters,
|
||||
chartConfiguration,
|
||||
colorScheme,
|
||||
ownColorScheme,
|
||||
colorNamespace,
|
||||
sliceId,
|
||||
dataMask,
|
||||
extraControls,
|
||||
labelsColor,
|
||||
labelsColorMap,
|
||||
sharedLabelsColors,
|
||||
allSliceIds,
|
||||
}: GetFormDataWithExtraFiltersArguments) {
|
||||
// if dashboard metadata + filters have not changed, use cache if possible
|
||||
const cachedFormData = cachedFormdataByChart[sliceId];
|
||||
if (
|
||||
cachedFiltersByChart[sliceId] === filters &&
|
||||
areObjectsEqual(cachedFormData?.color_scheme, colorScheme, {
|
||||
ignoreUndefined: true,
|
||||
}) &&
|
||||
areObjectsEqual(cachedFormData?.own_color_scheme, ownColorScheme) &&
|
||||
areObjectsEqual(cachedFormData?.color_scheme, colorScheme) &&
|
||||
areObjectsEqual(cachedFormData?.color_namespace, colorNamespace, {
|
||||
ignoreUndefined: true,
|
||||
}) &&
|
||||
areObjectsEqual(cachedFormData?.label_colors, labelsColor, {
|
||||
ignoreUndefined: true,
|
||||
}) &&
|
||||
areObjectsEqual(cachedFormData?.shared_label_colors, labelsColorMap, {
|
||||
areObjectsEqual(cachedFormData?.map_label_colors, labelsColorMap, {
|
||||
ignoreUndefined: true,
|
||||
}) &&
|
||||
isEqual(cachedFormData?.shared_label_colors, sharedLabelsColors) &&
|
||||
!!cachedFormData &&
|
||||
areObjectsEqual(cachedFormData?.dataMask, dataMask, {
|
||||
ignoreUndefined: true,
|
||||
@@ -110,9 +115,14 @@ export default function getFormDataWithExtraFilters({
|
||||
|
||||
const formData = {
|
||||
...chart.form_data,
|
||||
chart_id: chart.id,
|
||||
label_colors: labelsColor,
|
||||
shared_label_colors: labelsColorMap,
|
||||
shared_label_colors: sharedLabelsColors,
|
||||
map_label_colors: labelsColorMap,
|
||||
...(colorScheme && { color_scheme: colorScheme }),
|
||||
...(ownColorScheme && {
|
||||
own_color_scheme: ownColorScheme,
|
||||
}),
|
||||
extra_filters: getEffectiveExtraFilters(filters),
|
||||
...extraData,
|
||||
...extraControls,
|
||||
|
||||
@@ -58,6 +58,7 @@ const CHARTS = {
|
||||
datasource: '2__table',
|
||||
viz_type: 'echarts_timeseries_line',
|
||||
slice_id: 1,
|
||||
color_scheme: 'supersetColors',
|
||||
},
|
||||
chartAlert: null,
|
||||
chartStatus: 'rendered' as const,
|
||||
@@ -76,6 +77,7 @@ const CHARTS = {
|
||||
'2': {
|
||||
id: 2,
|
||||
form_data: {
|
||||
color_scheme: 'supersetColors',
|
||||
datasource: '2__table',
|
||||
viz_type: 'echarts_timeseries_line',
|
||||
slice_id: 2,
|
||||
@@ -262,8 +264,10 @@ test('Recalculate charts in global filter scope when charts change', () => {
|
||||
'3': {
|
||||
id: 3,
|
||||
form_data: {
|
||||
slice_id: 3,
|
||||
datasource: '3__table',
|
||||
viz_type: 'echarts_timeseries_line',
|
||||
color_scheme: 'supersetColors',
|
||||
},
|
||||
chartAlert: null,
|
||||
chartStatus: 'rendered' as const,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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]);
|
||||
@@ -76,6 +76,9 @@ const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
datasource_type: PropTypes.string.isRequired,
|
||||
dashboardId: PropTypes.number,
|
||||
colorScheme: PropTypes.string,
|
||||
ownColorScheme: PropTypes.string,
|
||||
dashboardColorScheme: PropTypes.string,
|
||||
isDatasourceMetaLoading: PropTypes.bool.isRequired,
|
||||
chart: chartPropShape.isRequired,
|
||||
slice: PropTypes.object,
|
||||
@@ -563,6 +566,7 @@ function ExploreViewContainer(props) {
|
||||
canOverwrite={props.can_overwrite}
|
||||
canDownload={props.can_download}
|
||||
dashboardId={props.dashboardId}
|
||||
colorScheme={props.dashboardColorScheme}
|
||||
isStarred={props.isStarred}
|
||||
slice={props.slice}
|
||||
sliceName={props.sliceName}
|
||||
@@ -741,6 +745,9 @@ function mapStateToProps(state) {
|
||||
},
|
||||
);
|
||||
const chart = charts[slice_id];
|
||||
const colorScheme = explore.form_data?.color_scheme;
|
||||
const ownColorScheme = explore.form_data?.own_color_scheme;
|
||||
const dashboardColorScheme = explore.form_data?.dashboard_color_scheme;
|
||||
|
||||
let dashboardId = Number(explore.form_data?.dashboardId);
|
||||
if (Number.isNaN(dashboardId)) {
|
||||
@@ -753,6 +760,9 @@ function mapStateToProps(state) {
|
||||
datasource_type: datasource.type,
|
||||
datasourceId: datasource.datasource_id,
|
||||
dashboardId,
|
||||
colorScheme,
|
||||
ownColorScheme,
|
||||
dashboardColorScheme,
|
||||
controls: explore.controls,
|
||||
can_add: !!explore.can_add,
|
||||
can_download: !!explore.can_download,
|
||||
|
||||
@@ -29,6 +29,7 @@ import ColorSchemeControl, { ColorSchemes } from '.';
|
||||
|
||||
const defaultProps = () => ({
|
||||
hasCustomLabelsColor: false,
|
||||
sharedLabelsColors: [],
|
||||
label: 'Color scheme',
|
||||
labelMargin: 0,
|
||||
name: 'color',
|
||||
@@ -128,8 +129,8 @@ test('displays color scheme options', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Renders control with dashboard id', () => {
|
||||
setup({ dashboardId: 1 });
|
||||
test('Renders control with dashboard id and dashboard color scheme', () => {
|
||||
setup({ dashboardId: 1, hasDashboardColorScheme: true });
|
||||
expect(screen.getByText('Dashboard scheme')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByLabelText('Select color scheme', { selector: 'input' }),
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
styled,
|
||||
t,
|
||||
useTheme,
|
||||
getLabelsColorMap,
|
||||
CategoricalColorNamespace,
|
||||
} from '@superset-ui/core';
|
||||
import AntdSelect from 'antd/lib/select';
|
||||
import { isFunction, sortBy } from 'lodash';
|
||||
@@ -35,6 +37,7 @@ import Icons from 'src/components/Icons';
|
||||
import { SelectOptionsType } from 'src/components/Select/types';
|
||||
import { StyledSelect } from 'src/components/Select/styles';
|
||||
import { handleFilterOptionHelper } from 'src/components/Select/utils';
|
||||
import { getColorNamespace } from 'src/utils/colorScheme';
|
||||
import ColorSchemeLabel from './ColorSchemeLabel';
|
||||
|
||||
const { Option, OptGroup } = AntdSelect;
|
||||
@@ -47,6 +50,12 @@ export interface ColorSchemes {
|
||||
|
||||
export interface ColorSchemeControlProps {
|
||||
hasCustomLabelsColor: boolean;
|
||||
hasDashboardColorScheme?: boolean;
|
||||
hasSharedLabelsColor?: boolean;
|
||||
sharedLabelsColors?: string[];
|
||||
mapLabelsColors?: Record<string, any>;
|
||||
colorNamespace?: string;
|
||||
chartId?: number;
|
||||
dashboardId?: number;
|
||||
label: string;
|
||||
name: string;
|
||||
@@ -64,8 +73,8 @@ const StyledAlert = styled(Icons.AlertSolid)`
|
||||
`;
|
||||
|
||||
const CUSTOM_LABEL_ALERT = t(
|
||||
`This color scheme is being overridden by custom label colors.
|
||||
Check the JSON metadata in the Advanced settings`,
|
||||
`The colors of this chart might be overridden by custom label colors of the related dashboard.
|
||||
Check the JSON metadata in the Advanced settings.`,
|
||||
);
|
||||
|
||||
const DASHBOARD_ALERT = t(
|
||||
@@ -73,18 +82,38 @@ const DASHBOARD_ALERT = t(
|
||||
Edit the color scheme in the dashboard properties.`,
|
||||
);
|
||||
|
||||
const DASHBOARD_CONTEXT_ALERT = t(
|
||||
`You are viewing this chart in a dashboard context with labels shared across multiple charts.
|
||||
The color scheme selection is disabled.`,
|
||||
);
|
||||
|
||||
const DASHBOARD_CONTEXT_TOOLTIP = t(
|
||||
`You are viewing this chart in the context of a dashboard that is directly affecting its colors.
|
||||
To edit the color scheme, open this chart outside of the dashboard.`,
|
||||
);
|
||||
|
||||
const Label = ({
|
||||
label,
|
||||
hasCustomLabelsColor,
|
||||
dashboardId,
|
||||
hasSharedLabelsColor,
|
||||
hasCustomLabelsColor,
|
||||
hasDashboardColorScheme,
|
||||
}: Pick<
|
||||
ColorSchemeControlProps,
|
||||
'label' | 'hasCustomLabelsColor' | 'dashboardId'
|
||||
| 'label'
|
||||
| 'dashboardId'
|
||||
| 'hasCustomLabelsColor'
|
||||
| 'hasSharedLabelsColor'
|
||||
| 'hasDashboardColorScheme'
|
||||
>) => {
|
||||
if (hasCustomLabelsColor || dashboardId) {
|
||||
const alertTitle = hasCustomLabelsColor
|
||||
? CUSTOM_LABEL_ALERT
|
||||
: DASHBOARD_ALERT;
|
||||
if (hasSharedLabelsColor || hasCustomLabelsColor || hasDashboardColorScheme) {
|
||||
const alertTitle =
|
||||
hasCustomLabelsColor && !hasSharedLabelsColor
|
||||
? CUSTOM_LABEL_ALERT
|
||||
: dashboardId && hasDashboardColorScheme
|
||||
? DASHBOARD_ALERT
|
||||
: DASHBOARD_CONTEXT_ALERT;
|
||||
|
||||
return (
|
||||
<>
|
||||
{label}{' '}
|
||||
@@ -99,7 +128,12 @@ const Label = ({
|
||||
|
||||
const ColorSchemeControl = ({
|
||||
hasCustomLabelsColor = false,
|
||||
hasDashboardColorScheme = false,
|
||||
mapLabelsColors = {},
|
||||
sharedLabelsColors = [],
|
||||
dashboardId,
|
||||
colorNamespace,
|
||||
chartId,
|
||||
label = t('Color scheme'),
|
||||
onChange = () => {},
|
||||
value,
|
||||
@@ -110,9 +144,21 @@ const ColorSchemeControl = ({
|
||||
isLinear,
|
||||
...rest
|
||||
}: ColorSchemeControlProps) => {
|
||||
const countSharedLabelsColor = sharedLabelsColors.length;
|
||||
const colorMapInstance = getLabelsColorMap();
|
||||
const chartLabels = chartId
|
||||
? colorMapInstance.chartsLabelsMap.get(chartId)?.labels || []
|
||||
: [];
|
||||
const hasSharedLabelsColor = !!(
|
||||
dashboardId &&
|
||||
countSharedLabelsColor > 0 &&
|
||||
chartLabels.some(label => sharedLabelsColors.includes(label))
|
||||
);
|
||||
const hasDashboardScheme = dashboardId && hasDashboardColorScheme;
|
||||
const showDashboardLockedOption = hasDashboardScheme || hasSharedLabelsColor;
|
||||
const theme = useTheme();
|
||||
const currentScheme = useMemo(() => {
|
||||
if (dashboardId) {
|
||||
if (showDashboardLockedOption) {
|
||||
return 'dashboard';
|
||||
}
|
||||
let result = value || defaultScheme;
|
||||
@@ -121,13 +167,15 @@ const ColorSchemeControl = ({
|
||||
result = schemesObject?.SUPERSET_DEFAULT?.id;
|
||||
}
|
||||
return result;
|
||||
}, [dashboardId, defaultScheme, schemes, value]);
|
||||
}, [defaultScheme, schemes, showDashboardLockedOption, value]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (dashboardId) {
|
||||
if (showDashboardLockedOption) {
|
||||
return [
|
||||
<Option value="dashboard" label={t('dashboard')} key="dashboard">
|
||||
<Tooltip title={DASHBOARD_ALERT}>{t('Dashboard scheme')}</Tooltip>
|
||||
<Option value="dashboard" label={t('Dashboard')} key="dashboard">
|
||||
<Tooltip title={DASHBOARD_CONTEXT_TOOLTIP}>
|
||||
{t('Dashboard scheme')}
|
||||
</Tooltip>
|
||||
</Option>,
|
||||
];
|
||||
}
|
||||
@@ -218,11 +266,29 @@ const ColorSchemeControl = ({
|
||||
))}
|
||||
</OptGroup>
|
||||
));
|
||||
}, [choices, dashboardId, isLinear, schemes]);
|
||||
}, [choices, hasDashboardScheme, hasSharedLabelsColor, isLinear, schemes]);
|
||||
|
||||
// We can't pass on change directly because it receives a second
|
||||
// parameter and it would be interpreted as the error parameter
|
||||
const handleOnChange = (value: string) => onChange(value);
|
||||
const handleOnChange = (value: string) => {
|
||||
if (chartId) {
|
||||
colorMapInstance.setOwnColorScheme(chartId, value);
|
||||
if (dashboardId) {
|
||||
const colorNameSpace = getColorNamespace(colorNamespace);
|
||||
const categoricalNamespace =
|
||||
CategoricalColorNamespace.getNamespace(colorNameSpace);
|
||||
|
||||
const sharedLabelsSet = new Set(sharedLabelsColors);
|
||||
// reset colors except shared and custom labels to keep dashboard consistency
|
||||
const resettableLabels = Object.keys(mapLabelsColors).filter(
|
||||
l => !sharedLabelsSet.has(l),
|
||||
);
|
||||
categoricalNamespace.resetColorsForLabels(resettableLabels);
|
||||
}
|
||||
}
|
||||
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -231,8 +297,10 @@ const ColorSchemeControl = ({
|
||||
label={
|
||||
<Label
|
||||
label={label}
|
||||
hasCustomLabelsColor={hasCustomLabelsColor}
|
||||
dashboardId={dashboardId}
|
||||
hasCustomLabelsColor={hasCustomLabelsColor}
|
||||
hasDashboardColorScheme={hasDashboardColorScheme}
|
||||
hasSharedLabelsColor={hasSharedLabelsColor}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -249,7 +317,7 @@ const ColorSchemeControl = ({
|
||||
`}
|
||||
aria-label={t('Select color scheme')}
|
||||
allowClear={clearable}
|
||||
disabled={!!dashboardId}
|
||||
disabled={hasDashboardScheme || hasSharedLabelsColor}
|
||||
onChange={handleOnChange}
|
||||
placeholder={t('Select scheme')}
|
||||
value={currentScheme}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { JsonArray, JsonValue, t } from '@superset-ui/core';
|
||||
import { ControlProps } from 'src/explore/components/Control';
|
||||
import builtInControlComponents from 'src/explore/components/controls';
|
||||
import useEffectEvent from 'src/hooks/useEffectEvent';
|
||||
|
||||
/**
|
||||
* Full control component map.
|
||||
@@ -72,7 +73,7 @@ export type AsyncVerify = (
|
||||
* Whether the extra props will update the original props.
|
||||
*/
|
||||
function hasUpdates(
|
||||
props: ControlPropsWithExtras,
|
||||
props: Partial<ControlPropsWithExtras>,
|
||||
newProps: ExtraControlProps,
|
||||
) {
|
||||
return (
|
||||
@@ -165,17 +166,17 @@ export default function withAsyncVerification({
|
||||
[basicOnChange, otherProps, verifiedProps],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (needAsyncVerification && verify) {
|
||||
const verifyProps = useEffectEvent(
|
||||
(verifyFunc: AsyncVerify, props: typeof otherProps) => {
|
||||
if (showLoadingState) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
verify(otherProps)
|
||||
verifyFunc(props)
|
||||
.then(updatedProps => {
|
||||
if (showLoadingState) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
if (updatedProps && hasUpdates(otherProps, updatedProps)) {
|
||||
if (updatedProps && hasUpdates(verifiedProps, updatedProps)) {
|
||||
setVerifiedProps({
|
||||
// save isLoading in combination with other props to avoid
|
||||
// rendering twice.
|
||||
@@ -198,14 +199,14 @@ export default function withAsyncVerification({
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (needAsyncVerification && verify) {
|
||||
verifyProps(verify, otherProps);
|
||||
}
|
||||
}, [
|
||||
needAsyncVerification,
|
||||
showLoadingState,
|
||||
verify,
|
||||
otherProps,
|
||||
addWarningToast,
|
||||
]);
|
||||
}, [needAsyncVerification, verify, otherProps, verifyProps]);
|
||||
|
||||
return (
|
||||
<ControlComponent
|
||||
|
||||
@@ -61,6 +61,9 @@ describe('controlUtils', () => {
|
||||
controls: {},
|
||||
form_data: { datasource: '1__table', viz_type: 'table' },
|
||||
common: {},
|
||||
slice: {
|
||||
slice_id: 1,
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -151,10 +151,9 @@ const getExpectedResultFormData = (overrides: JsonObject = {}) => ({
|
||||
girl: '#FF69B4',
|
||||
boy: '#ADD8E6',
|
||||
},
|
||||
shared_label_colors: {
|
||||
boy: '#ADD8E6',
|
||||
girl: '#FF69B4',
|
||||
},
|
||||
shared_label_colors: ['boy', 'girl'],
|
||||
own_color_scheme: 'supersetColors',
|
||||
dashboard_color_scheme: 'd3Category20b',
|
||||
extra_filters: [
|
||||
{
|
||||
col: '__time_range',
|
||||
|
||||
@@ -218,11 +218,18 @@ export const getFormDataWithDashboardContext = (
|
||||
{},
|
||||
);
|
||||
|
||||
const ownColorScheme = exploreFormData.color_scheme;
|
||||
const dashboardColorScheme = dashboardContextFormData.color_scheme;
|
||||
const appliedColorScheme = dashboardColorScheme || ownColorScheme;
|
||||
|
||||
return {
|
||||
...exploreFormData,
|
||||
...dashboardContextFormData,
|
||||
...filterBoxData,
|
||||
...nativeFiltersData,
|
||||
...adhocFilters,
|
||||
own_color_scheme: ownColorScheme,
|
||||
color_scheme: appliedColorScheme,
|
||||
dashboard_color_scheme: dashboardColorScheme,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,6 +82,10 @@ export interface ExplorePageInitialData {
|
||||
owners: string[];
|
||||
created_by?: string;
|
||||
changed_by?: string;
|
||||
dashboards?: {
|
||||
id: number;
|
||||
dashboard_title: string;
|
||||
}[];
|
||||
};
|
||||
saveAction?: SaveActionType | null;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||