Compare commits

..

1 Commits

Author SHA1 Message Date
Beto Dealmeida
795b1714d2 chore: add unit tests for is_mutating() 2024-11-21 10:50:14 -05:00
381 changed files with 6411 additions and 8173 deletions

View File

@@ -53,9 +53,6 @@ github:
merge: false
rebase: false
ghp_branch: gh-pages
ghp_path: /
protected_branches:
master:
required_status_checks:
@@ -91,10 +88,3 @@ github:
required_approving_review_count: 1
required_signatures: false
gh-pages:
required_pull_request_reviews:
dismiss_stale_reviews: false
require_code_owner_reviews: true
required_approving_review_count: 1
required_signatures: false

1
.gitattributes vendored
View File

@@ -1,3 +1,2 @@
docker/**/*.sh text eol=lf
*.svg binary
*.ipynb binary

View File

@@ -42,7 +42,7 @@ runs:
- name: Install dependencies
run: |
if [ "${{ inputs.install-superset }}" = "true" ]; then
sudo apt-get update && sudo apt-get -y install libldap2-dev libsasl2-dev build-essential
sudo apt-get update && sudo apt-get -y install libldap2-dev libsasl2-dev
pip install --upgrade pip setuptools wheel
if [ "${{ inputs.requirements-type }}" = "dev" ]; then
pip install -r requirements/development.txt

View File

@@ -14,12 +14,6 @@ on:
required: true
description: Max number of PRs to open (0 for no limit)
default: 5
extra-flags:
required: false
default: --only-base
description: Additional flags to pass to the bump-python command
#schedule:
# - cron: '0 0 * * *' # Runs daily at midnight UTC
jobs:
bump-python-package:
@@ -65,13 +59,10 @@ jobs:
GROUP_OPT="-g ${{ github.event.inputs.group }}"
fi
EXTRA_FLAGS="${{ github.event.inputs.extra-flags }}"
supersetbot bump-python \
--verbose \
--use-current-repo \
--include-subpackages \
--limit ${{ github.event.inputs.limit }} \
$PACKAGE_OPT \
$GROUP_OPT \
$EXTRA_FLAGS
$GROUP_OPT

View File

@@ -1,25 +1,30 @@
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.inputs.issue_number || github.event.issue.number || github.run_id }}-comment
group: ${{ github.workflow }}-${{ 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:
@@ -39,18 +44,18 @@ jobs:
with:
result-encoding: string
script: |
const pattern = /^\/testenv (up|down)/;
const result = pattern.exec('${{ github.event.inputs.comment_body || github.event.comment.body }}');
return result === null ? 'noop' : result[1];
const pattern = /^\/testenv (up|down)/
const result = pattern.exec(context.payload.comment.body)
return result === null ? 'noop' : result[1]
- name: Looking for feature flags
- name: Eval comment body for feature flags
uses: actions/github-script@v7
id: eval-feature-flags
with:
script: |
const pattern = /FEATURE_(\w+)=(\w+)/g;
let results = [];
[...'${{ github.event.inputs.comment_body || github.event.comment.body }}'.matchAll(pattern)].forEach(match => {
[...context.payload.comment.body.matchAll(pattern)].forEach(match => {
const config = {
name: `SUPERSET_FEATURE_${match[1]}`,
value: match[2],
@@ -62,48 +67,24 @@ 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);
- name: Reply with confirmation comment
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issueNumber = ${{ github.event.inputs.issue_number || github.event.issue.number }};
const user = '${{ github.event.comment.user.login || github.actor }}';
const action = '${{ steps.eval-body.outputs.result }}';
const runId = context.runId;
const workflowUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
const body = action === 'noop'
? `@${user} No ephemeral environment action detected. Please use '/testenv up' or '/testenv down'. [View workflow run](${workflowUrl}).`
: `@${user} Processing your ephemeral environment request [here](${workflowUrl}).`;
if (action !== 'noop') {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body,
});
}
})
core.setFailed(errMsg)
ephemeral-docker-build:
concurrency:
group: ${{ github.workflow }}-${{ github.event.inputs.issue_number || github.event.issue.number || github.run_id }}-build
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}-build
cancel-in-progress: true
needs: ephemeral-env-comment
name: ephemeral-docker-build
@@ -117,9 +98,9 @@ jobs:
const request = {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.inputs.issue_number || github.event.issue.number }},
};
core.info(`Getting PR #${request.pull_number} from ${request.owner}/${request.repo}`);
pull_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;
@@ -140,17 +121,12 @@ 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: |
supersetbot docker \
--preset ci \
--platform linux/amd64 \
--context-ref "$RELEASE"
./scripts/build_docker.py \
"ci" \
"pull_request" \
--build_context_ref ${{ github.event.issue.number }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
@@ -170,7 +146,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.inputs.issue_number || github.event.issue.number }}-ci
docker tag $IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:pr-${{ github.event.issue.number }}-ci
docker push -a $ECR_REGISTRY/$ECR_REPOSITORY
ephemeral-env-up:
@@ -205,22 +181,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.inputs.issue_number || github.event.issue.number }}-ci
--image-ids imageTag=pr-${{ 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.inputs.issue_number || github.event.issue.number }},
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
@@ -228,7 +204,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.inputs.issue_number || github.event.issue.number }}-ci
image: ${{ steps.login-ecr.outputs.registry }}/superset-ci:pr-${{ github.event.issue.number }}-ci
- name: Update env vars in the Amazon ECS task definition
run: |
@@ -237,29 +213,30 @@ jobs:
- name: Describe ECS service
id: describe-services
run: |
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
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${{ github.event.issue.number }}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
- name: Create ECS service
id: create-service
if: steps.describe-services.outputs.active != 'true'
id: create-service
env:
ECR_SUBNETS: subnet-0e15a5034b4121710,subnet-0e8efef4a72224974
ECR_SECURITY_GROUP: sg-092ff3a6ae0574d91
run: |
aws ecs create-service \
--cluster superset-ci \
--service-name pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-service \
--service-name pr-${{ github.event.issue.number }}-service \
--task-definition superset-ci \
--launch-type FARGATE \
--desired-count 1 \
--platform-version LATEST \
--network-configuration "awsvpcConfiguration={subnets=[$ECR_SUBNETS],securityGroups=[$ECR_SECURITY_GROUP],assignPublicIp=ENABLED}" \
--tags key=pr,value=${{ github.event.inputs.issue_number || github.event.issue.number }} key=github_user,value=${{ github.actor }}
--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
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-service
service: pr-${{ github.event.issue.number }}-service
cluster: superset-ci
wait-for-service-stability: true
wait-for-minutes: 10
@@ -267,15 +244,18 @@ jobs:
- name: List tasks
id: list-tasks
run: |
echo "task=$(aws ecs list-tasks --cluster superset-ci --service-name pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-service | jq '.taskArns | first')" >> $GITHUB_OUTPUT
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
@@ -283,11 +263,12 @@ jobs:
github-token: ${{github.token}}
script: |
github.rest.issues.createComment({
issue_number: ${{ github.event.inputs.issue_number || github.event.issue.number }},
issue_number: ${{ github.event.issue.number }},
owner: context.repo.owner,
repo: context.repo.repo,
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.'
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.'
})
- name: Comment (failure)
if: ${{ failure() }}
uses: actions/github-script@v7
@@ -295,8 +276,8 @@ jobs:
github-token: ${{github.token}}
script: |
github.rest.issues.createComment({
issue_number: ${{ github.event.inputs.issue_number || github.event.issue.number }},
issue_number: ${{ github.event.issue.number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: '@${{ github.event.inputs.user_login || github.event.comment.user.login }} Ephemeral environment creation failed. Please check the Actions logs for details.'
body: '@${{ github.event.comment.user.login }} Ephemeral environment creation failed. Please check the Actions logs for details.'
})

View File

@@ -1,4 +1,4 @@
name: "Helm: lint and test charts"
name: Lint and Test Charts
on:
pull_request:

View File

@@ -1,8 +1,4 @@
# This workflow automates the release process for Helm charts.
# The workflow creates a new branch for the release and opens a pull request against the 'gh-pages' branch,
# allowing the changes to be reviewed and merged manually.
name: "Helm: release charts"
name: Release Charts
on:
push:
@@ -11,28 +7,18 @@ on:
- "[0-9].[0-9]*"
paths:
- "helm/**"
workflow_dispatch:
inputs:
ref:
description: "The branch, tag, or commit SHA to check out"
required: false
default: "master"
jobs:
release:
runs-on: ubuntu-22.04
permissions:
contents: write
pull-requests: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout code
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref_name }}
persist-credentials: true
persist-credentials: false
submodules: recursive
fetch-depth: 0
@@ -49,77 +35,11 @@ jobs:
- name: Add bitnami repo dependency
run: helm repo add bitnami https://charts.bitnami.com/bitnami
- name: Fetch/list all tags
run: |
# Debugging tags
git fetch --tags --force
git tag -d superset-helm-chart-0.13.4 || true
echo "DEBUG TAGS"
git show-ref --tags
- name: Create unique pages branch name
id: vars
run: echo "branch_name=helm-publish-${GITHUB_SHA:0:7}" >> $GITHUB_ENV
- name: Force recreate branch from gh-pages
run: |
# Ensure a clean working directory
git reset --hard
git clean -fdx
git checkout -b local_gha_temp
git submodule update
# Fetch the latest gh-pages branch
git fetch origin gh-pages
# Check out and reset the target branch based on gh-pages
git checkout -B ${{ env.branch_name }} origin/gh-pages
# Remove submodules from the branch
git submodule deinit -f --all
# Force push to the remote branch
git push origin ${{ env.branch_name }} --force
# Return to the original branch
git checkout local_gha_temp
- name: Fetch/list all tags
run: |
git submodule update
cat .github/actions/chart-releaser-action/action.yml
- name: Run chart-releaser
uses: ./.github/actions/chart-releaser-action
with:
version: v1.6.0
charts_dir: helm
mark_as_latest: false
pages_branch: ${{ env.branch_name }}
env:
CR_TOKEN: "${{ github.token }}"
CR_RELEASE_NAME_TEMPLATE: "superset-helm-chart-{{ .Version }}"
- name: Open Pull Request
uses: actions/github-script@v7
with:
script: |
const branchName = '${{ env.branch_name }}';
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
if (!branchName) {
throw new Error("Branch name is not defined.");
}
const pr = await github.rest.pulls.create({
owner,
repo,
title: `Helm chart release for ${branchName}`,
head: branchName,
base: "gh-pages", // Adjust if the target branch is different
body: `This PR releases Helm charts to the gh-pages branch.`,
});
core.info(`Pull request created: ${pr.data.html_url}`);
env:
BRANCH_NAME: ${{ env.branch_name }}

View File

@@ -142,7 +142,6 @@ jobs:
- name: Python unit tests (PostgreSQL)
if: steps.check.outputs.python
run: |
pip install -e .[hive]
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
- name: Upload code coverage
uses: codecov/codecov-action@v4

View File

@@ -16,11 +16,11 @@
#
repos:
- repo: https://github.com/MarcoGorelli/auto-walrus
rev: 0.3.4
rev: v0.2.2
hooks:
- id: auto-walrus
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
rev: v1.3.0
hooks:
- id: mypy
args: [--check-untyped-defs]
@@ -39,11 +39,11 @@ repos:
types-Markdown,
]
- repo: https://github.com/peterdemin/pip-compile-multi
rev: v2.6.4
rev: v2.6.2
hooks:
- id: pip-compile-multi-verify
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v4.4.0
hooks:
- id: check-docstring-first
- id: check-added-large-files
@@ -56,7 +56,7 @@ repos:
exclude: ^.*\.(snap)
args: ["--markdown-linebreak-ext=md"]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8 # Use the sha or tag you want to point at
rev: v3.1.0 # Use the sha or tag you want to point at
hooks:
- id: prettier
additional_dependencies:
@@ -70,12 +70,12 @@ repos:
- id: blacklist
args: ["--blacklisted-names=make_url", "--ignore=tests/"]
- repo: https://github.com/norwoodj/helm-docs
rev: v1.14.2
rev: v1.11.0
hooks:
- id: helm-docs
files: helm
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
rev: v0.4.0
hooks:
- id: ruff
args: [ --fix ]

View File

@@ -115,7 +115,7 @@ RUN mkdir -p ${PYTHONPATH} superset/static requirements superset-frontend apache
libldap2-dev \
&& touch superset/static/version_info.json \
&& chown -R superset:superset ./* \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
&& rm -rf /var/lib/apt/lists/*
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/* /var/cache/apt/archives/*
&& rm -rf /var/lib/apt/lists/*
# 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/cache/apt/archives/* /var/lib/apt/lists/*
&& rm -rf /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/* /var/cache/apt/archives/*; \
&& apt-get autoremove -yqq --purge wget bzip2 && rm -rf /var/[log,tmp]/* /tmp/* /var/lib/apt/lists/*; \
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/* /var/cache/apt/archives/*
&& rm -rf /var/lib/apt/lists/*
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/* /var/cache/apt/archives/*
&& rm -rf /var/lib/apt/lists/*
USER superset
######################################################################

View File

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

View File

@@ -25,6 +25,7 @@ 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
@@ -130,6 +131,23 @@ 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
@@ -161,7 +179,6 @@ services:
# set this to false if you have perf issues running the npm i; npm run dev in-docker
# if you do so, you have to run this manually on the host, which should perform better!
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
NPM_RUN_PRUNE: false
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
container_name: superset_node
command: ["/app/docker/docker-frontend.sh"]

View File

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

View File

@@ -53,14 +53,11 @@ To send alerts and reports to Slack channels, you need to create a new Slack App
- `incoming-webhook`
- `files:write`
- `chat:write`
- `channels:read`
- `groups:read`
4. At the top of the "OAuth and Permissions" section, click "install to workspace".
5. Select a default channel for your app and continue.
(You can post to any channel by inviting your Superset app into that channel).
6. The app should now be installed in your workspace, and a "Bot User OAuth Access Token" should have been created. Copy that token in the `SLACK_API_TOKEN` variable of your `superset_config.py`.
7. Ensure the feature flag `ALERT_REPORT_SLACK_V2` is set to True in `superset_config.py`
8. Restart the service (or run `superset init`) to pull in the new configuration.
7. Restart the service (or run `superset init`) to pull in the new configuration.
Note: when you configure an alert or a report, the Slack channel list takes channel names without the leading '#' e.g. use `alerts` instead of `#alerts`.

View File

@@ -455,6 +455,17 @@ pre-commit install
A series of checks will now run when you make a git commit.
Alternatively, it is possible to run pre-commit via tox:
```bash
tox -e pre-commit
```
Or by running pre-commit manually:
```bash
pre-commit run --all-files
```
## Linting
@@ -463,7 +474,8 @@ A series of checks will now run when you make a git commit.
We use [Pylint](https://pylint.org/) for linting which can be invoked via:
```bash
pylint
# for python
tox -e pylint
```
In terms of best practices please avoid blanket disabling of Pylint messages globally (via `.pylintrc`) or top-level within the file header, albeit there being a few exceptions. Disabling should occur inline as it prevents masking issues and provides context as to why said message is disabled.
@@ -490,108 +502,39 @@ If using the eslint extension with vscode, put the following in your workspace `
]
```
## GitHub Actions and `act`
:::tip
`act` compatibility of Superset's GHAs is not fully tested. Running `act` locally may or may not
work for different actions, and may require fine tunning and local secret-handling.
For those more intricate GHAs that are tricky to run locally, we recommend iterating
directly on GHA's infrastructure, by pushing directly on a branch and monitoring GHA logs.
For more targetted iteration, see the `gh workflow run --ref {BRANCH}` subcommand of the GitHub CLI.
:::
For automation and CI/CD, Superset makes extensive use of GitHub Actions (GHA). You
can find all of the workflows and other assets under the `.github/` folder. This includes:
- running the backend unit test suites (`tests/`)
- running the frontend test suites (`superset-frontend/src/**.*.test.*`)
- running our Cypress end-to-end tests (`superset-frontend/cypress-base/`)
- linting the codebase, including all Python, Typescript and Javascript, yaml and beyond
- checking for all sorts of other rules conventions
When you open a pull request (PR), the appropriate GitHub Actions (GHA) workflows will
automatically run depending on the changes in your branch. It's perfectly reasonable
(and required!) to rely on this automation. However, the downside is that it's mostly an
all-or-nothing approach and doesn't provide much control to target specific tests or
iterate quickly.
At times, it may be more convenient to run GHA workflows locally. For that purpose
we use [act](https://github.com/nektos/act), a tool that allows you to run GitHub Actions (GHA)
workflows locally. It simulates the GitHub Actions environment, enabling developers to
test and debug workflows on their local machines before pushing changes to the repository. More
on how to use it in the next section.
:::note
In both GHA and `act`, we can run a more complex matrix for our tests, executing against different
database engines (PostgreSQL, MySQL, SQLite) and different versions of Python.
This enables us to ensure compatibility and stability across various environments.
:::
### Using `act`
First, install `act` -> https://nektosact.com/
To list the workflows, simply:
```bash
act --list
```
To run a specific workflow:
```bash
act pull_request --job {workflow_name} --secret GITHUB_TOKEN=$GITHUB_TOKEN --container-architecture linux/amd64
```
In the example above, notice that:
- we target a specific workflow, using `--job`
- we pass a secret using `--secret`, as many jobs require read access (public) to the repo
- we simulate a `pull_request` event by specifying it as the first arg,
similarly, we could simulate a `push` event or something else
- we specify `--container-architecture`, which tends to emulate GHA more reliably
:::note
`act` is a rich tool that offers all sorts of features, allowing you to simulate different
events (pull_request, push, ...), semantics around passing secrets where required and much
more. For more information, refer to [act's documentation](https://nektosact.com/)
:::
:::note
Some jobs require secrets to interact with external systems and accounts that you may
not have in your possession. In those cases you may have to rely on remote CI or parameterize the
job further to target a different environment/sandbox or your own alongside the related
secrets.
:::
---
## Testing
### Python Testing
#### Unit Tests
For unit tests located in `tests/unit_tests/`, it's usually easy to simply run the script locally using:
All python tests are carried out in [tox](https://tox.readthedocs.io/en/latest/index.html)
a standardized testing framework.
All python tests can be run with any of the tox [environments](https://tox.readthedocs.io/en/latest/example/basic.html#a-simple-tox-ini-default-environments), via,
```bash
pytest tests/unit_tests/*
tox -e <environment>
```
#### Integration Tests
For more complex pytest-defined integration tests (not to be confused with our end-to-end Cypress tests), many tests will require having a working test environment. Some tests require a database, Celery, and potentially other services or libraries installed.
### Running Tests with `act`
To run integration tests locally using `act`, ensure you have followed the setup instructions from the [GitHub Actions and `act`](#github-actions-and-act) section. You can run specific workflows or jobs that include integration tests. For example:
For example,
```bash
act --job test-python-38 --secret GITHUB_TOKEN=$GITHUB_TOKEN --event pull_request --container-architecture linux/amd64
tox -e py38
```
#### Running locally using a test script
Alternatively, you can run all tests in a single file via,
There is also a utility script included in the Superset codebase to run Python integration tests. The [readme can be found here](https://github.com/apache/superset/tree/master/scripts/tests).
```bash
tox -e <environment> -- tests/test_file.py
```
or for a specific test via,
```bash
tox -e <environment> -- tests/test_file.py::TestClassName::test_method_name
```
Note that the test environment uses a temporary directory for defining the
SQLite databases which will be cleared each time before the group of test
commands are invoked.
There is also a utility script included in the Superset codebase to run python integration tests. The [readme can be
found here](https://github.com/apache/superset/tree/master/scripts/tests)
@@ -602,7 +545,7 @@ To run all integration tests, for example, run this script from the root directo
scripts/tests/run.sh
```
You can run unit tests found in `./tests/unit_tests` with pytest. It is a simple way to run an isolated test that doesn't need any database setup:
You can run unit tests found in './tests/unit_tests' for example with pytest. It is a simple way to run an isolated test that doesn't need any database setup
```bash
pytest ./link_to_test.py
@@ -625,7 +568,7 @@ npm run test -- path/to/file.js
### Integration Testing
We use [Cypress](https://www.cypress.io/) for integration tests. To open Cypress and explore tests first setup and run test server:
We use [Cypress](https://www.cypress.io/) for integration tests. Tests can be run by `tox -e cypress`. To open Cypress and explore tests first setup and run test server:
```bash
export SUPERSET_CONFIG=tests.integration_tests.superset_test_config

View File

@@ -170,10 +170,31 @@ npm run dev-server
### Python Testing
`pytest`, backend by docker-compose is how we recommend running tests locally.
All python tests are carried out in [tox](https://tox.readthedocs.io/en/latest/index.html)
a standardized testing framework.
All python tests can be run with any of the tox [environments](https://tox.readthedocs.io/en/latest/example/basic.html#a-simple-tox-ini-default-environments), via,
For a more complex test matrix (against different database backends, python versions, ...) you
can rely on our GitHub Actions by simply opening a draft pull request.
```bash
tox -e <environment>
```
For example,
```bash
tox -e py38
```
Alternatively, you can run all tests in a single file via,
```bash
tox -e <environment> -- tests/test_file.py
```
or for a specific test via,
```bash
tox -e <environment> -- tests/test_file.py::TestClassName::test_method_name
```
Note that the test environment uses a temporary directory for defining the
SQLite databases which will be cleared each time before the group of test
@@ -225,7 +246,13 @@ npm run test -- path/to/file.js
### e2e Integration Testing
For e2e testing, we recommend that you use a `docker-compose` backed-setup
We use [Cypress](https://www.cypress.io/) for end-to-end integration
tests. One easy option to get started quickly is to leverage `tox` to
run the whole suite in an isolated environment.
```bash
tox -e cypress
```
Alternatively, you can go lower level and set things up in your
development environment by following these steps:
@@ -571,31 +598,17 @@ pybabel compile -d superset/translations
### Python
We use [Pylint](https://pylint.org/) and [ruff](https://github.com/astral-sh/ruff)
for linting which can be invoked via:
We use [Pylint](https://pylint.org/) for linting which can be invoked via:
```
# Run pylint
pylint superset/
# auto-reformat using ruff
ruff format
# lint check with ruff
ruff check
# lint fix with ruff
ruff check --fix
```bash
# for python
tox -e pylint
```
In terms of best practices please avoid blanket disabling of Pylint messages globally (via `.pylintrc`) or top-level within the file header, albeit there being a few exceptions. Disabling should occur inline as it prevents masking issues and provides context as to why said message is disabled.
In terms of best practices please avoid blanket disabling of Pylint messages globally
(via `.pylintrc`) or top-level within the file header, albeit there being a few exceptions.
Disabling should occur inline as it prevents masking issues and provides context as to why
said message is disabled.
All this is configured to run in pre-commit hooks, which we encourage you to setup
with `pre-commit install`
Additionally, the Python code is auto-formatted using [Black](https://github.com/python/black) which
is configured as a pre-commit hook. There are also numerous [editor integrations](https://black.readthedocs.io/en/stable/integrations/editors.html)
### TypeScript

View File

@@ -29,7 +29,7 @@ We have a set of build "presets" that each represent a combination of
parameters for the build, mostly pointing to either different target layer
for the build, and/or base image.
Here are the build presets that are exposed through the `supersetbot docker` utility:
Here are the build presets that are exposed through the `build_docker.py` script:
- `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 `supersetbot docker` uti
For insights or modifications to the build matrix and tagging conventions,
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)
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)
GitHub action.
## Key ARGs in Dockerfile

View File

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

View File

@@ -77,6 +77,10 @@ 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
@@ -130,22 +134,21 @@ First, start by installing `apache-superset`:
pip install apache-superset
```
Then, define mandatory configurations, SECRET_KEY and FLASK_APP:
```bash
export SUPERSET_SECRET_KEY=YOUR-SECRET-KEY
export FLASK_APP=superset
```
Then, you need to initialize the database:
```bash
superset db upgrade
```
:::tip
Note that some configuration is mandatory for production instances of Superset. In particular, Superset will not start without a user-specified value of SECRET_KEY. Please see [Configuring Superset](/docs/configuration/configuring-superset).
:::
Finish installing by running through the following commands:
```bash
# Create an admin user in your metadata database (use `admin` as username to be able to load the examples)
export FLASK_APP=superset
superset fab create-admin
# Load some data to play with

View File

@@ -27,34 +27,33 @@ 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 **Settings ‣ Data ‣ Database Connections**. Find the **examples** database in the list and
In the top menu, select **Data ‣ Databases**. 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 **Advanced** tab and open **Security** section.
Then, tick the checkbox for **Allow file uploads to database**. End by clicking the **Finish** button.
In the resulting modal window, switch to the **Extra** tab and
tick the checkbox for **Allow Data Upload**. End by clicking the **Save** button.
<img src={useBaseUrl("/img/tutorial/allow-file-uploads.png" )} />
<img src={useBaseUrl("/img/tutorial/add-data-upload.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 top menu, select **Settings ‣ Data ‣ Database Connections**. Then, **Upload file to database ‣ Upload CSV**.
In the Superset menu, select **Data ‣ Upload a CSV**.
<img src={useBaseUrl("/img/tutorial/upload_a_csv.png" )} />
Then, select select the CSV file from your computer, select **Database** and **Schema**, and enter the **Table Name**
as _tutorial_flights_.
Then, enter the **Table Name** as _tutorial_flights_ and select the CSV file from your computer.
<img src={useBaseUrl("/img/tutorial/csv_to_database_configuration.png" )} />
Next enter the text _Travel Date_ into the **File settings ‣ Columns to be parsed as dates** field.
Next enter the text _Travel Date_ into the **Parse Dates** field.
<img src={useBaseUrl("/img/tutorial/parse_dates_column.png" )} />
Leaving all the other options in their default settings, select **Upload** at the bottom of the page.
Leaving all the other options in their default settings, select **Save** at the bottom of the page.
### Table Visualization

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.13.4
version: 0.13.3
dependencies:
- name: postgresql
version: 12.1.6

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.13.4](https://img.shields.io/badge/Version-0.13.4-informational?style=flat-square)
![Version: 0.13.3](https://img.shields.io/badge/Version-0.13.3-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application

View File

@@ -19,8 +19,7 @@
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# A README is automatically generated from this file to document it,
# using helm-docs (see https://github.com/norwoodj/helm-docs)
# A README is automatically generated from this file to document it, using helm-docs (see https://github.com/norwoodj/helm-docs)
# To update it, install helm-docs and run helm-docs from the root of this chart
# -- Provide a name to override the name of the chart

View File

@@ -69,7 +69,7 @@ dependencies = [
"nh3>=0.2.11, <0.3",
"numpy==1.23.5",
"packaging",
"pandas[excel,performance]>=2.0.3, <2.1",
"pandas[performance]>=2.0.3, <2.1",
"parsedatetime",
"paramiko>=3.4.0",
"pgsanity",
@@ -90,9 +90,7 @@ dependencies = [
"slack_sdk>=3.19.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy-utils>=0.38.3, <0.39",
# known breaking changes in sqlglot 25.25.0
#https://github.com/tobymao/sqlglot/blob/main/CHANGELOG.md#v25250---2024-10-14
"sqlglot>=25.24.0,<25.25.0",
"sqlglot>=25.24.0,<26",
"sqlparse>=0.5.0",
"tabulate>=0.8.9, <0.9",
"typing-extensions>=4, <5",
@@ -137,6 +135,7 @@ gevent = ["gevent>=23.9.1"]
gsheets = ["shillelagh[gsheetsapi]>=1.2.18, <2"]
hana = ["hdbcli==2.4.162", "sqlalchemy_hana==0.4.0"]
hive = [
"boto3",
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7.0",
"tableschema",
@@ -159,7 +158,7 @@ pinot = ["pinotdb>=5.0.0, <6.0.0"]
playwright = ["playwright>=1.37.0, <2"]
postgres = ["psycopg2-binary==2.9.6"]
presto = ["pyhive[presto]>=0.6.5"]
trino = ["trino>=0.328.0"]
trino = ["boto3", "trino>=0.328.0"]
prophet = ["prophet>=1.1.5, <2"]
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
rockset = ["rockset-sqlalchemy>=0.0.1, <1"]
@@ -182,10 +181,7 @@ development = [
"docker",
"flask-testing",
"freezegun",
# playwright requires greenlet==3.0.3
# submitted a PR to relax deps in 11/2024
# https://github.com/microsoft/playwright-python/pull/2669
"greenlet==3.0.3",
"greenlet>=2.0.2",
"grpcio>=1.55.3",
"openapi-spec-validator",
"parameterized",
@@ -203,6 +199,7 @@ development = [
"ruff",
"sqloxide",
"statsd",
"tox",
]
[project.urls]
@@ -239,10 +236,172 @@ disallow_untyped_calls = false
disallow_untyped_defs = false
disable_error_code = "annotation-unchecked"
[tool.tox]
legacy_tox_ini = """
# Remember to start celery workers to run celery tests, e.g.
# celery --app=superset.tasks.celery_app:app worker -Ofair -c 2
[testenv]
basepython = python3.10
ignore_basepython_conflict = true
commands =
superset db upgrade
superset init
superset load-test-users
# use -s to be able to use break pointers.
# no args or tests/* can be passed as an argument to run all tests
pytest -s {posargs}
deps =
-rrequirements/development.txt
setenv =
PYTHONPATH = {toxinidir}
SUPERSET_TESTENV = true
SUPERSET_CONFIG = tests.integration_tests.superset_test_config
SUPERSET_HOME = {envtmpdir}
mysql: SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://mysqluser:mysqluserpassword@localhost/superset?charset=utf8
postgres: SUPERSET__SQLALCHEMY_DATABASE_URI = postgresql+psycopg2://superset:superset@localhost/test
sqlite: SUPERSET__SQLALCHEMY_DATABASE_URI = sqlite:////{envtmpdir}/superset.db
sqlite: SUPERSET__SQLALCHEMY_EXAMPLES_URI = sqlite:////{envtmpdir}/examples.db
mysql-presto: SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://mysqluser:mysqluserpassword@localhost/superset?charset=utf8
# docker run -p 8080:8080 --name presto starburstdata/presto
mysql-presto: SUPERSET__SQLALCHEMY_EXAMPLES_URI = presto://localhost:8080/memory/default
# based on https://github.com/big-data-europe/docker-hadoop
# clone the repo & run docker compose up -d to test locally
mysql-hive: SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://mysqluser:mysqluserpassword@localhost/superset?charset=utf8
mysql-hive: SUPERSET__SQLALCHEMY_EXAMPLES_URI = hive://localhost:10000/default
# make sure that directory is accessible by docker
hive: UPLOAD_FOLDER = /tmp/.superset/app/static/uploads/
usedevelop = true
allowlist_externals =
npm
pkill
[testenv:cypress]
setenv =
PYTHONPATH = {toxinidir}
SUPERSET_TESTENV = true
SUPERSET_CONFIG = tests.integration_tests.superset_test_config
SUPERSET_HOME = {envtmpdir}
commands =
npm install -g npm@'>=6.5.0'
pip install -e {toxinidir}/
{toxinidir}/superset-frontend/cypress_build.sh
commands_post =
pkill -if "python {envbindir}/flask"
[testenv:cypress-dashboard]
setenv =
PYTHONPATH = {toxinidir}
SUPERSET_TESTENV = true
SUPERSET_CONFIG = tests.integration_tests.superset_test_config
SUPERSET_HOME = {envtmpdir}
commands =
npm install -g npm@'>=6.5.0'
pip install -e {toxinidir}/
{toxinidir}/superset-frontend/cypress_build.sh dashboard
commands_post =
pkill -if "python {envbindir}/flask"
[testenv:cypress-explore]
setenv =
PYTHONPATH = {toxinidir}
SUPERSET_TESTENV = true
SUPERSET_CONFIG = tests.integration_tests.superset_test_config
SUPERSET_HOME = {envtmpdir}
commands =
npm install -g npm@'>=6.5.0'
pip install -e {toxinidir}/
{toxinidir}/superset-frontend/cypress_build.sh explore
commands_post =
pkill -if "python {envbindir}/flask"
[testenv:cypress-sqllab]
setenv =
PYTHONPATH = {toxinidir}
SUPERSET_TESTENV = true
SUPERSET_CONFIG = tests.integration_tests.superset_test_config
SUPERSET_HOME = {envtmpdir}
commands =
npm install -g npm@'>=6.5.0'
pip install -e {toxinidir}/
{toxinidir}/superset-frontend/cypress_build.sh sqllab
commands_post =
pkill -if "python {envbindir}/flask"
[testenv:cypress-sqllab-backend-persist]
setenv =
PYTHONPATH = {toxinidir}
SUPERSET_TESTENV = true
SUPERSET_CONFIG = tests.integration_tests.superset_test_config
SUPERSET_HOME = {envtmpdir}
commands =
npm install -g npm@'>=6.5.0'
pip install -e {toxinidir}/
{toxinidir}/superset-frontend/cypress_build.sh sqllab
commands_post =
pkill -if "python {envbindir}/flask"
[testenv:eslint]
changedir = {toxinidir}/superset-frontend
commands =
npm run lint
deps =
[testenv:fossa]
commands =
{toxinidir}/scripts/fossa.sh
deps =
passenv = *
[testenv:javascript]
commands =
npm install -g npm@'>=6.5.0'
{toxinidir}/superset-frontend/js_build.sh
deps =
[testenv:license-check]
commands =
{toxinidir}/scripts/check_license.sh
passenv = *
whitelist_externals =
{toxinidir}/scripts/check_license.sh
deps =
[testenv:pre-commit]
commands =
pre-commit run --all-files
deps =
-rrequirements/development.txt
skip_install = true
[testenv:pylint]
commands =
pylint superset
deps =
-rrequirements/development.txt
[testenv:thumbnails]
setenv =
SUPERSET_CONFIG = tests.integration_tests.superset_test_config_thumbnails
deps =
-rrequirements/development.txt
[tox]
envlist =
cypress-dashboard
cypress-explore
cypress-sqllab
cypress-sqllab-backend-persist
eslint
fossa
javascript
license-check
pre-commit
pylint
skipsdist = true
"""
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
"**/*.ipynb",
".bzr",
".direnv",
".eggs",

View File

@@ -20,7 +20,3 @@
urllib3>=1.26.18
werkzeug>=3.0.1
numexpr>=2.9.0
# 5.0.0 has a sensitive deprecation used in other libs
# -> https://github.com/aio-libs/async-timeout/blob/master/CHANGES.rst#500-2024-10-31
async_timeout>=4.0.0,<5.0.0

View File

@@ -1,4 +1,4 @@
# SHA1:cc62b2b6658afa9dbb6e81046e1084f15442858a
# SHA1:85649679306ea016e401f37adfbad832028d2e5f
#
# This file is autogenerated by pip-compile-multi
# To update, run:
@@ -9,32 +9,30 @@
# via -r requirements/base.in
alembic==1.13.1
# via flask-migrate
amqp==5.3.1
amqp==5.2.0
# via kombu
apispec[yaml]==6.3.0
# via flask-appbuilder
apsw==3.46.0.0
# via shillelagh
async-timeout==4.0.3
# via
# -r requirements/base.in
# redis
attrs==24.2.0
# via redis
attrs==23.2.0
# via
# cattrs
# jsonschema
# requests-cache
babel==2.16.0
babel==2.15.0
# via flask-babel
backoff==2.2.1
# via apache-superset
bcrypt==4.2.1
bcrypt==4.1.3
# via paramiko
billiard==4.2.1
billiard==4.2.0
# via celery
blinker==1.9.0
blinker==1.8.2
# via flask
bottleneck==1.4.2
bottleneck==1.3.8
# via pandas
brotli==1.1.0
# via flask-compress
@@ -42,19 +40,19 @@ cachelib==0.9.0
# via
# flask-caching
# flask-session
cachetools==5.5.0
cachetools==5.3.3
# via google-auth
cattrs==24.1.2
cattrs==23.2.3
# via requests-cache
celery==5.4.0
# via apache-superset
certifi==2024.8.30
certifi==2024.2.2
# via requests
cffi==1.17.1
cffi==1.16.0
# via
# cryptography
# pynacl
charset-normalizer==3.4.0
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via
@@ -78,27 +76,23 @@ colorama==0.4.6
# via
# apache-superset
# flask-appbuilder
cron-descriptor==1.4.5
cron-descriptor==1.4.3
# via apache-superset
croniter==5.0.1
croniter==2.0.5
# via apache-superset
cryptography==43.0.3
cryptography==42.0.8
# via
# apache-superset
# paramiko
# pyopenssl
defusedxml==0.7.1
# via odfpy
deprecated==1.2.15
deprecated==1.2.14
# via limits
deprecation==2.1.0
# via apache-superset
dnspython==2.7.0
dnspython==2.6.1
# via email-validator
email-validator==2.2.0
email-validator==2.1.1
# via flask-appbuilder
et-xmlfile==2.0.0
# via openpyxl
exceptiongroup==1.2.2
# via cattrs
flask==2.3.3
@@ -121,11 +115,11 @@ flask-babel==2.0.0
# via flask-appbuilder
flask-caching==2.3.0
# via apache-superset
flask-compress==1.17
flask-compress==1.15
# via apache-superset
flask-jwt-extended==4.7.1
flask-jwt-extended==4.6.0
# via flask-appbuilder
flask-limiter==3.8.0
flask-limiter==3.7.0
# via flask-appbuilder
flask-login==0.6.3
# via
@@ -141,7 +135,7 @@ flask-sqlalchemy==2.5.1
# flask-migrate
flask-talisman==1.1.0
# via apache-superset
flask-wtf==1.2.2
flask-wtf==1.2.1
# via
# apache-superset
# flask-appbuilder
@@ -151,29 +145,27 @@ geographiclib==2.0
# via geopy
geopy==2.4.1
# via apache-superset
google-auth==2.36.0
google-auth==2.29.0
# via shillelagh
greenlet==3.1.1
# via
# shillelagh
# sqlalchemy
gunicorn==23.0.0
greenlet==3.0.3
# via shillelagh
gunicorn==22.0.0
# via apache-superset
hashids==1.3.1
# via apache-superset
holidays==0.25
# via apache-superset
humanize==4.11.0
humanize==4.9.0
# via apache-superset
idna==3.10
idna==3.7
# via
# email-validator
# requests
importlib-metadata==8.5.0
importlib-metadata==7.1.0
# via apache-superset
importlib-resources==6.4.5
importlib-resources==6.4.0
# via limits
isodate==0.7.2
isodate==0.6.1
# via apache-superset
itsdangerous==2.2.0
# via
@@ -183,33 +175,33 @@ jinja2==3.1.4
# via
# flask
# flask-babel
jsonpath-ng==1.7.0
jsonpath-ng==1.6.1
# via apache-superset
jsonschema==4.17.3
# via flask-appbuilder
kombu==5.4.2
kombu==5.3.7
# via celery
korean-lunar-calendar==0.3.1
# via holidays
limits==3.13.0
limits==3.12.0
# via flask-limiter
llvmlite==0.43.0
llvmlite==0.42.0
# via numba
mako==1.3.6
mako==1.3.5
# via
# alembic
# apache-superset
markdown==3.7
markdown==3.6
# via apache-superset
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
markupsafe==2.1.5
# via
# jinja2
# mako
# werkzeug
# wtforms
marshmallow==3.23.1
marshmallow==3.21.2
# via
# flask-appbuilder
# marshmallow-sqlalchemy
@@ -221,11 +213,11 @@ msgpack==1.0.8
# via apache-superset
msgspec==0.18.6
# via flask-session
nh3==0.2.18
nh3==0.2.17
# via apache-superset
numba==0.60.0
numba==0.59.1
# via pandas
numexpr==2.10.2
numexpr==2.10.0
# via
# -r requirements/base.in
# pandas
@@ -237,13 +229,9 @@ numpy==1.23.5
# numexpr
# pandas
# pyarrow
odfpy==1.4.1
# via pandas
openpyxl==3.1.5
# via pandas
ordered-set==4.1.0
# via flask-limiter
packaging==24.2
packaging==23.2
# via
# apache-superset
# apispec
@@ -253,9 +241,9 @@ packaging==24.2
# marshmallow
# marshmallow-sqlalchemy
# shillelagh
pandas[excel,performance]==2.0.3
pandas[performance]==2.0.3
# via apache-superset
paramiko==3.5.0
paramiko==3.4.0
# via
# apache-superset
# sshtunnel
@@ -271,30 +259,30 @@ polyline==2.0.2
# via apache-superset
prison==0.2.1
# via flask-appbuilder
prompt-toolkit==3.0.48
prompt-toolkit==3.0.44
# via click-repl
pyarrow==14.0.2
# via apache-superset
pyasn1==0.6.1
pyasn1==0.6.0
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.4.1
pyasn1-modules==0.4.0
# via google-auth
pycparser==2.22
# via cffi
pygments==2.18.0
# via rich
pyjwt==2.10.0
pyjwt==2.8.0
# via
# apache-superset
# flask-appbuilder
# flask-jwt-extended
pynacl==1.5.0
# via paramiko
pyopenssl==24.2.1
pyopenssl==24.1.0
# via shillelagh
pyparsing==3.2.0
pyparsing==3.1.2
# via apache-superset
pyrsistent==0.20.0
# via jsonschema
@@ -311,14 +299,12 @@ python-dotenv==1.0.1
# via apache-superset
python-geohash==0.8.5
# via apache-superset
pytz==2024.2
pytz==2024.1
# via
# croniter
# flask-babel
# pandas
pyxlsb==1.0.10
# via pandas
pyyaml==6.0.2
pyyaml==6.0.1
# via
# apache-superset
# apispec
@@ -330,7 +316,7 @@ requests==2.32.2
# shillelagh
requests-cache==1.2.0
# via shillelagh
rich==13.9.4
rich==13.7.1
# via flask-limiter
rsa==4.9
# via google-auth
@@ -340,17 +326,18 @@ shillelagh[gsheetsapi]==1.2.18
# via apache-superset
shortid==0.1.2
# via apache-superset
simplejson==3.19.3
simplejson==3.19.2
# via apache-superset
six==1.16.0
# via
# isodate
# prison
# python-dateutil
# url-normalize
# wtforms-json
slack-sdk==3.33.4
slack-sdk==3.27.2
# via apache-superset
sqlalchemy==1.4.54
sqlalchemy==1.4.52
# via
# alembic
# apache-superset
@@ -363,27 +350,25 @@ sqlalchemy-utils==0.38.3
# via
# apache-superset
# flask-appbuilder
sqlglot==25.24.5
sqlglot==25.24.0
# via apache-superset
sqlparse==0.5.2
sqlparse==0.5.0
# via apache-superset
sshtunnel==0.4.0
# via apache-superset
tabulate==0.8.10
# via apache-superset
typing-extensions==4.12.2
typing-extensions==4.12.0
# via
# alembic
# apache-superset
# cattrs
# flask-limiter
# limits
# rich
# shillelagh
tzdata==2024.2
tzdata==2024.1
# via
# celery
# kombu
# pandas
url-normalize==1.4.3
# via requests-cache
@@ -400,14 +385,14 @@ vine==5.1.0
# kombu
wcwidth==0.2.13
# via prompt-toolkit
werkzeug==3.1.3
werkzeug==3.0.6
# via
# -r requirements/base.in
# flask
# flask-appbuilder
# flask-jwt-extended
# flask-login
wrapt==1.17.0
wrapt==1.16.0
# via deprecated
wtforms==3.2.1
# via
@@ -417,13 +402,9 @@ wtforms==3.2.1
# wtforms-json
wtforms-json==0.3.5
# via apache-superset
xlrd==2.0.1
# via pandas
xlsxwriter==3.0.9
# via
# apache-superset
# pandas
zipp==3.21.0
# via apache-superset
zipp==3.19.0
# via importlib-metadata
zstandard==0.23.0
zstandard==0.22.0
# via flask-compress

View File

@@ -17,4 +17,4 @@
# under the License.
#
-r base.in
-e .[development,bigquery,cors,druid,gevent,gsheets,mysql,playwright,postgres,presto,prophet,trino,thumbnails]
-e .[development,bigquery,cors,druid,gevent,gsheets,hive,mysql,playwright,postgres,presto,prophet,trino,thumbnails]

View File

@@ -1,4 +1,4 @@
# SHA1:dc767a7288b56c785b0cd3c38e95e7b5e66be1ac
# SHA1:c186006a3f82c8775e1039f37c52309f6c858197
#
# This file is autogenerated by pip-compile-multi
# To update, run:
@@ -12,69 +12,89 @@
# -r requirements/development.in
astroid==3.1.0
# via pylint
boto3==1.34.112
# via
# apache-superset
# dataflows-tabulator
botocore==1.34.112
# via
# boto3
# s3transfer
build==1.2.1
# via pip-tools
cfgv==3.4.0
cached-property==1.5.2
# via tableschema
cfgv==3.3.1
# via pre-commit
chardet==5.1.0
# via
# dataflows-tabulator
# tox
cmdstanpy==1.1.0
# via prophet
contourpy==1.0.7
# via matplotlib
coverage[toml]==7.6.8
coverage[toml]==7.2.5
# via pytest-cov
cycler==0.12.1
cycler==0.11.0
# via matplotlib
db-dtypes==1.3.1
dataflows-tabulator==1.54.3
# via tableschema
db-dtypes==1.2.0
# via pandas-gbq
dill==0.3.9
dill==0.3.8
# via pylint
distlib==0.3.8
# via virtualenv
docker==7.0.0
# via apache-superset
et-xmlfile==1.1.0
# via openpyxl
filelock==3.12.2
# via virtualenv
# via
# tox
# virtualenv
flask-cors==4.0.0
# via apache-superset
flask-testing==0.8.1
# via apache-superset
fonttools==4.55.0
fonttools==4.51.0
# via matplotlib
freezegun==1.5.1
# via apache-superset
future==1.0.0
future==0.18.3
# via pyhive
gevent==24.2.1
# via apache-superset
google-api-core[grpc]==2.23.0
google-api-core[grpc]==2.11.0
# via
# google-cloud-bigquery
# google-cloud-bigquery-storage
# google-cloud-core
# pandas-gbq
# sqlalchemy-bigquery
google-auth-oauthlib==1.2.1
google-auth-oauthlib==1.0.0
# via
# pandas-gbq
# pydata-google-auth
google-cloud-bigquery==3.27.0
google-cloud-bigquery==3.20.1
# via
# apache-superset
# pandas-gbq
# sqlalchemy-bigquery
google-cloud-bigquery-storage==2.19.1
# via pandas-gbq
google-cloud-core==2.4.1
google-cloud-core==2.3.2
# via google-cloud-bigquery
google-crc32c==1.6.0
google-crc32c==1.5.0
# via google-resumable-media
google-resumable-media==2.7.2
google-resumable-media==2.7.0
# via google-cloud-bigquery
googleapis-common-protos==1.66.0
googleapis-common-protos==1.63.0
# via
# google-api-core
# grpcio-status
grpcio==1.68.0
grpcio==1.62.1
# via
# apache-superset
# google-api-core
@@ -83,21 +103,31 @@ grpcio-status==1.60.1
# via google-api-core
identify==2.5.36
# via pre-commit
ijson==3.2.3
# via dataflows-tabulator
iniconfig==2.0.0
# via pytest
isort==5.12.0
# via pylint
jmespath==1.0.1
# via
# boto3
# botocore
jsonlines==4.0.0
# via dataflows-tabulator
jsonschema-spec==0.1.6
# via openapi-spec-validator
kiwisolver==1.4.7
kiwisolver==1.4.5
# via matplotlib
lazy-object-proxy==1.10.0
# via openapi-spec-validator
linear-tsv==1.1.0
# via dataflows-tabulator
matplotlib==3.9.0
# via prophet
mccabe==0.7.0
# via pylint
mysqlclient==2.2.6
mysqlclient==2.2.4
# via apache-superset
nodeenv==1.8.0
# via pre-commit
@@ -107,6 +137,8 @@ openapi-schema-validator==0.4.4
# via openapi-spec-validator
openapi-spec-validator==0.5.6
# via apache-superset
openpyxl==3.1.2
# via dataflows-tabulator
pandas-gbq==0.19.1
# via apache-superset
parameterized==0.9.0
@@ -123,30 +155,32 @@ pip-tools==7.4.1
# via pip-compile-multi
playwright==1.42.0
# via apache-superset
pluggy==1.5.0
# via pytest
pre-commit==4.0.1
pluggy==1.4.0
# via
# pytest
# tox
pre-commit==3.7.1
# via apache-superset
progress==1.6
# via apache-superset
prophet==1.1.5
# via apache-superset
proto-plus==1.25.0
# via
# google-api-core
# google-cloud-bigquery-storage
protobuf==4.25.5
proto-plus==1.22.2
# via google-cloud-bigquery-storage
protobuf==4.23.0
# via
# google-api-core
# google-cloud-bigquery-storage
# googleapis-common-protos
# grpcio-status
# proto-plus
psutil==6.1.0
psutil==6.0.0
# via apache-superset
psycopg2-binary==2.9.6
# via apache-superset
pydata-google-auth==1.9.0
pure-sasl==0.6.2
# via thrift-sasl
pydata-google-auth==1.7.0
# via pandas-gbq
pydruid==0.6.9
# via apache-superset
@@ -160,7 +194,9 @@ pyinstrument==4.4.0
# via apache-superset
pylint==3.1.0
# via apache-superset
pyproject-hooks==1.2.0
pyproject-api==1.6.1
# via tox
pyproject-hooks==1.0.0
# via
# build
# pip-tools
@@ -169,7 +205,7 @@ pytest==7.4.4
# apache-superset
# pytest-cov
# pytest-mock
pytest-cov==6.0.0
pytest-cov==5.0.0
# via apache-superset
pytest-mock==3.10.0
# via apache-superset
@@ -179,37 +215,62 @@ requests-oauthlib==2.0.0
# via google-auth-oauthlib
rfc3339-validator==0.1.4
# via openapi-schema-validator
ruff==0.8.0
rfc3986==2.0.0
# via tableschema
ruff==0.4.5
# via apache-superset
sqlalchemy-bigquery==1.12.0
s3transfer==0.10.1
# via boto3
sqlalchemy-bigquery==1.11.0
# via apache-superset
sqloxide==0.1.51
sqloxide==0.1.43
# via apache-superset
statsd==4.0.1
# via apache-superset
tomli==2.1.0
tableschema==1.20.10
# via apache-superset
thrift==0.16.0
# via
# apache-superset
# thrift-sasl
thrift-sasl==0.4.3
# via apache-superset
tomli==2.0.1
# via
# build
# coverage
# pip-tools
# pylint
# pyproject-api
# pyproject-hooks
# pytest
tomlkit==0.13.2
# tox
tomlkit==0.12.5
# via pylint
toposort==1.10
# via pip-compile-multi
tqdm==4.67.1
tox==4.6.4
# via apache-superset
tqdm==4.66.4
# via
# cmdstanpy
# prophet
trino==0.330.0
trino==0.328.0
# via apache-superset
tzlocal==5.2
# via trino
unicodecsv==0.14.1
# via
# dataflows-tabulator
# tableschema
virtualenv==20.23.1
# via pre-commit
wheel==0.45.1
# via
# pre-commit
# tox
wheel==0.43.0
# via pip-tools
xlrd==2.0.1
# via dataflows-tabulator
zope-event==5.0
# via gevent
zope-interface==5.4.0

294
scripts/build_docker.py Executable file
View File

@@ -0,0 +1,294 @@
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import os
import re
import subprocess
from textwrap import dedent
import click
REPO = "apache/superset"
CACHE_REPO = f"{REPO}-cache"
BASE_PY_IMAGE = "3.10-slim-bookworm"
def run_cmd(command: str, raise_on_failure: bool = True) -> str:
process = subprocess.Popen(
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
output = ""
if process.stdout is not None:
for line in iter(process.stdout.readline, ""):
print(line.strip()) # Print the line to stdout in real-time
output += line
process.wait() # Wait for the subprocess to finish
if process.returncode != 0 and raise_on_failure:
raise subprocess.CalledProcessError(process.returncode, command, output)
return output
def get_git_sha() -> str:
return run_cmd("git rev-parse HEAD").strip()
def get_build_context_ref(build_context: str) -> str:
"""
Given a context, return a ref:
- if context is pull_request, return the PR's id
- if context is push, return the branch
- if context is release, return the release ref
"""
event = os.getenv("GITHUB_EVENT_NAME")
github_ref = os.getenv("GITHUB_REF", "")
if event == "pull_request":
github_head_ref = os.getenv("GITHUB_HEAD_REF", "")
return re.sub("[^a-zA-Z0-9]", "-", github_head_ref)[:40]
elif event == "release":
return re.sub("refs/tags/", "", github_ref)[:40]
elif event == "push":
return re.sub("[^a-zA-Z0-9]", "-", re.sub("refs/heads/", "", github_ref))[:40]
return ""
def is_latest_release(release: str) -> bool:
output = (
run_cmd(
f"./scripts/tag_latest_release.sh {release} --dry-run",
raise_on_failure=False,
)
or ""
)
return "SKIP_TAG::false" in output
def make_docker_tag(l: list[str]) -> str: # noqa: E741
return f"{REPO}:" + "-".join([o for o in l if o])
def get_docker_tags(
build_preset: str,
build_platforms: list[str],
sha: str,
build_context: str,
build_context_ref: str,
force_latest: bool = False,
) -> set[str]:
"""
Return a set of tags given a given build context
"""
tags: set[str] = set()
tag_chunks: list[str] = []
is_latest = is_latest_release(build_context_ref)
if build_preset != "lean":
# Always add the preset_build name if different from default (lean)
tag_chunks += [build_preset]
if len(build_platforms) == 1:
build_platform = build_platforms[0]
short_build_platform = build_platform.replace("linux/", "").replace("64", "")
if short_build_platform != "amd":
# Always a platform indicator if different from default (amd)
tag_chunks += [short_build_platform]
# Always craft a tag for the SHA
tags.add(make_docker_tag([sha] + tag_chunks))
# also a short SHA, cause it's nice
tags.add(make_docker_tag([sha[:7]] + tag_chunks))
if build_context == "release":
# add a release tag
tags.add(make_docker_tag([build_context_ref] + tag_chunks))
if is_latest or force_latest:
# add a latest tag
tags.add(make_docker_tag(["latest"] + tag_chunks))
elif build_context == "push" and build_context_ref == "master":
tags.add(make_docker_tag(["master"] + tag_chunks))
elif build_context == "pull_request":
tags.add(make_docker_tag([f"pr-{build_context_ref}"] + tag_chunks))
return tags
def get_docker_command(
build_preset: str,
build_platforms: list[str],
is_authenticated: bool,
sha: str,
build_context: str,
build_context_ref: str,
force_latest: bool = False,
) -> str:
tag = "" # noqa: F841
build_target = ""
py_ver = BASE_PY_IMAGE
docker_context = "."
if build_preset == "dev":
build_target = "dev"
elif build_preset == "lean":
build_target = "lean"
elif build_preset == "py311":
build_target = "lean"
py_ver = "3.11-slim-bookworm"
elif build_preset == "websocket":
build_target = ""
docker_context = "superset-websocket"
elif build_preset == "ci":
build_target = "ci"
elif build_preset == "dockerize":
build_target = ""
docker_context = "-f dockerize.Dockerfile ."
else:
print(f"Invalid build preset: {build_preset}")
exit(1)
# Try to get context reference if missing
if not build_context_ref:
build_context_ref = get_build_context_ref(build_context)
tags = get_docker_tags(
build_preset,
build_platforms,
sha,
build_context,
build_context_ref,
force_latest,
)
docker_tags = ("\\\n" + 8 * " ").join([f"-t {s} " for s in tags])
docker_args = "--load" if not is_authenticated else "--push"
target_argument = f"--target {build_target}" if build_target else ""
cache_ref = f"{CACHE_REPO}:{py_ver}"
if len(build_platforms) == 1:
build_platform = build_platforms[0]
short_build_platform = build_platform.replace("linux/", "").replace("64", "")
cache_ref = f"{CACHE_REPO}:{py_ver}-{short_build_platform}"
platform_arg = "--platform " + ",".join(build_platforms)
cache_from_arg = f"--cache-from=type=registry,ref={cache_ref}"
cache_to_arg = (
f"--cache-to=type=registry,mode=max,ref={cache_ref}" if is_authenticated else ""
)
build_arg = f"--build-arg PY_VER={py_ver}" if py_ver else ""
actor = os.getenv("GITHUB_ACTOR")
return dedent(
f"""\
docker buildx build \\
{docker_args} \\
{docker_tags} \\
{cache_from_arg} \\
{cache_to_arg} \\
{build_arg} \\
{platform_arg} \\
{target_argument} \\
--label sha={sha} \\
--label target={build_target} \\
--label build_trigger={build_context} \\
--label base={py_ver} \\
--label build_actor={actor} \\
{docker_context}"""
)
@click.command()
@click.argument(
"build_preset",
type=click.Choice(["lean", "dev", "dockerize", "websocket", "py311", "ci"]),
)
@click.argument("build_context", type=click.Choice(["push", "pull_request", "release"]))
@click.option(
"--platform",
type=click.Choice(["linux/arm64", "linux/amd64"]),
default=["linux/amd64"],
multiple=True,
)
@click.option("--build_context_ref", help="a reference to the pr, release or branch")
@click.option("--dry-run", is_flag=True, help="Run the command in dry-run mode.")
@click.option("--verbose", is_flag=True, help="Print more info")
@click.option(
"--force-latest", is_flag=True, help="Force the 'latest' tag on the release"
)
def main(
build_preset: str,
build_context: str,
build_context_ref: str,
platform: list[str],
dry_run: bool,
force_latest: bool,
verbose: bool,
) -> None:
"""
This script executes docker build and push commands based on given arguments.
"""
is_authenticated = (
True if os.getenv("DOCKERHUB_TOKEN") and os.getenv("DOCKERHUB_USER") else False
)
if force_latest and build_context != "release":
print(
"--force-latest can only be applied if the build context is set to 'release'"
)
exit(1)
if build_context == "release" and not build_context_ref.strip():
print("Release number has to be provided")
exit(1)
docker_build_command = get_docker_command(
build_preset,
platform,
is_authenticated,
get_git_sha(),
build_context,
build_context_ref,
force_latest,
)
if not dry_run:
print("Executing Docker Build Command:")
print(docker_build_command)
script = ""
if os.getenv("DOCKERHUB_USER"):
script = dedent(
f"""\
docker logout
docker login --username "{os.getenv("DOCKERHUB_USER")}" --password "{os.getenv("DOCKERHUB_TOKEN")}"
DOCKER_ARGS="--push"
"""
)
script = script + docker_build_command
if verbose:
run_cmd("cat Dockerfile")
stdout = run_cmd(script) # noqa: F841
else:
print("Dry Run - Docker Build Command:")
print(docker_build_command)
if __name__ == "__main__":
main()

View File

@@ -95,21 +95,15 @@ def print_files(files: List[str]) -> None:
print("\n".join([f"- {s}" for s in files]))
def is_int(s: str) -> bool:
return bool(re.match(r"^-?\d+$", s))
def main(event_type: str, sha: str, repo: str) -> None:
"""Main function to check for file changes based on event context."""
print("SHA:", sha)
print("EVENT_TYPE", event_type)
files = None
if event_type == "pull_request":
pr_number = os.getenv("GITHUB_REF", "").split("/")[-2]
if is_int(pr_number):
files = fetch_changed_files_pr(repo, pr_number)
print("PR files:")
print_files(files)
files = fetch_changed_files_pr(repo, pr_number)
print("PR files:")
print_files(files)
elif event_type == "push":
files = fetch_changed_files_push(repo, sha)
@@ -125,7 +119,7 @@ def main(event_type: str, sha: str, repo: str) -> None:
changes_detected = {}
for group, regex_patterns in PATTERNS.items():
patterns_compiled = [re.compile(p) for p in regex_patterns]
changes_detected[group] = files is None or detect_changes(
changes_detected[group] = event_type == "workflow_dispatch" or detect_changes(
files, patterns_compiled
)

View File

@@ -27,11 +27,6 @@ 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:
@@ -113,30 +108,6 @@ Example `POST /security/guest_token` payload:
]
}
```
Alternatively, a guest token can be created directly in your app with a json like the following, and then signed
with the secret set in configuration variable `GUEST_TOKEN_JWT_SECRET` (see configuration file config.py)
```
{
"user": {
"username": "embedded@embedded.fr",
"first_name": "embedded",
"last_name": "embedded"
},
"resources": [
{
"type": "dashboard",
"id": "d73e7841-9342-4afd-8e29-b4a416a2498c"
}
],
"rls_rules": [],
"iat": 1730883214,
"exp": 1732956814,
"aud": "superset",
"type": "guest"
}
```
### Sandbox iframe
The Embedded SDK creates an iframe with [sandbox](https://developer.mozilla.org/es/docs/Web/HTML/Element/iframe#sandbox) mode by default

View File

@@ -87,7 +87,7 @@ describe('Charts list', () => {
visitChartList();
cy.getBySel('count-crosslinks').should('be.visible');
cy.getBySel('crosslinks').first().trigger('mouseover');
cy.get('.antd5-tooltip')
cy.get('.ant-tooltip')
.contains('3 - Sample dashboard')
.invoke('removeAttr', 'target')
.click();

View File

@@ -16,19 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
SAMPLE_DASHBOARD_1,
SUPPORTED_CHARTS_DASHBOARD,
TABBED_DASHBOARD,
} from 'cypress/utils/urls';
import { SAMPLE_DASHBOARD_1, TABBED_DASHBOARD } from 'cypress/utils/urls';
import { drag, resize, waitForChartLoad } from 'cypress/utils';
import * as ace from 'brace';
import {
interceptExploreUpdate,
interceptGet,
interceptUpdate,
openTab,
} from './utils';
import { interceptGet, interceptUpdate, openTab } from './utils';
import {
interceptExploreJson,
interceptFiltering as interceptCharts,
@@ -51,37 +42,15 @@ function openProperties() {
cy.getBySel('header-actions-menu')
.contains('Edit properties')
.click({ force: true });
cy.get('.ant-modal-body').should('be.visible');
cy.wait(500);
});
}
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(
@@ -114,36 +83,20 @@ 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 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') {
function resetTabbedDashboard(go = false) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cy.getDashboard(dashboard).then((r: Record<string, any>) => {
cy.getDashboard('tabbed_dash').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: [],
map_label_colors: {},
shared_label_colors: {},
});
cy.updateDashboard(r.id, {
certification_details: r.certification_details,
@@ -153,37 +106,27 @@ function resetDashboardColors(dashboard = 'tabbed_dash') {
json_metadata: resetMetadata,
owners: r.owners,
slug: r.slug,
}).then(() => {
if (go) {
visitEdit(TABBED_DASHBOARD);
}
});
});
}
function selectColorScheme(
color: string,
target = 'dashboard-edit-properties-form',
) {
cy.get(`[data-test="${target}"] input[aria-label="Select color scheme"]`)
function visitResetTabbedDashboard() {
resetTabbedDashboard(true);
}
function selectColorScheme(color: string) {
cy.get(
'[data-test="dashboard-edit-properties-form"] [aria-label="Select color scheme"]',
)
.first()
.then($input => {
cy.wrap($input).click({ force: true });
cy.wrap($input).type(color.slice(0, 5), { force: true });
});
.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() {
cy.getBySel('properties-modal-apply-button').click({ force: true });
}
@@ -194,37 +137,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({ force: true });
cy.wrap($jsonmetadata).find('.ace_content').click();
cy.wrap($jsonmetadata)
.find('.ace_text-input')
.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 });
});
.type('{selectall} {backspace}', { force: true });
});
}
function writeMetadata(metadata: string) {
cy.get('#json_metadata').then($jsonmetadata => {
cy.wrap($jsonmetadata).find('.ace_content').click({ force: true });
cy.wrap($jsonmetadata)
cy.get('#json_metadata').then($jsonmetadata =>
cy
.wrap($jsonmetadata)
.find('.ace_text-input')
.then($ace => {
cy.wrap($ace).focus();
cy.wrap($ace).should('have.focus');
cy.wrap($ace).type(metadata, {
parseSpecialCharSequences: false,
force: true,
});
});
});
.type(metadata, { parseSpecialCharSequences: false, force: true }),
);
}
function openExploreWithDashboardContext(chartName: string) {
function openExplore(chartName: string) {
interceptExploreJson();
interceptGet();
@@ -238,351 +181,23 @@ function openExploreWithDashboardContext(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(() => {
resetDashboardColors();
visitResetTabbedDashboard();
});
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)');
after(() => {
resetTabbedDashboard();
});
it('should respect chart color scheme when none is set for the dashboard', () => {
visit(TABBED_DASHBOARD);
openProperties();
cy.get('[aria-label="Select color scheme"]').should('have.value', '');
applyChanges();
saveChanges();
// open nested tab
openTab(1, 1);
@@ -599,8 +214,7 @@ describe('Dashboard edit', () => {
.should('have.css', 'fill', 'rgb(31, 168, 201)');
});
it('should apply same color to same labels with color scheme set on refresh', () => {
visitEdit(TABBED_DASHBOARD);
it('should apply same color to same labels with color scheme set', () => {
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@@ -624,63 +238,17 @@ 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 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);
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();
// open nested tab
openTab(1, 1);
@@ -707,7 +275,6 @@ describe('Dashboard edit', () => {
});
it('custom label colors should take the precedence in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
@@ -738,7 +305,6 @@ 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({
@@ -767,7 +333,6 @@ describe('Dashboard edit', () => {
});
it('should re-apply original color after removing custom label color with color scheme set', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
@@ -810,7 +375,6 @@ 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({
@@ -874,7 +438,6 @@ describe('Dashboard edit', () => {
});
it('should show the same colors in Explore', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
@@ -898,7 +461,7 @@ describe('Dashboard edit', () => {
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openExploreWithDashboardContext('Top 10 California Names Timeseries');
openExplore('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
@@ -906,8 +469,7 @@ describe('Dashboard edit', () => {
.should('have.css', 'fill', 'rgb(255, 0, 0)');
});
it('should change color scheme multiple times', () => {
visitEdit(TABBED_DASHBOARD);
it.skip('should change color scheme multiple times', () => {
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@@ -925,7 +487,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
// open 2nd main tab
openTab(0, 1);
@@ -934,7 +496,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
editDashboard();
openProperties();
@@ -945,7 +507,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(0, 128, 246)');
.should('have.css', 'fill', 'rgb(41, 105, 107)');
// open main tab and nested tab
openTab(0, 0);
@@ -956,11 +518,10 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(0, 128, 246)');
.should('have.css', 'fill', 'rgb(41, 105, 107)');
});
it('should apply the color scheme across main tabs', () => {
visitEdit(TABBED_DASHBOARD);
it.skip('should apply the color scheme across main tabs', () => {
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@@ -972,11 +533,10 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
});
it('should apply the color scheme across main tabs for rendered charts', () => {
visitEdit(TABBED_DASHBOARD);
it.skip('should apply the color scheme across main tabs for rendered charts', () => {
waitForChartLoad({ name: 'Treemap', viz: 'treemap_v2' });
openProperties();
selectColorScheme('blueToGreen');
@@ -989,7 +549,7 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
.should('have.css', 'fill', 'rgb(41, 105, 107)');
// change scheme now that charts are rendered across the main tabs
editDashboard();
@@ -1000,11 +560,10 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(0, 128, 246)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
});
it('should apply the color scheme in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
it.skip('should apply the color scheme in nested tabs', () => {
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@@ -1020,18 +579,17 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
// 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(50, 0, 167)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
});
it('should apply a valid color scheme for rendered charts in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
it.skip('should apply a valid color scheme for rendered charts in nested tabs', () => {
// open the tab first time and let chart load
openTab(1, 1);
waitForChartLoad({
@@ -1053,7 +611,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
});
});
@@ -1065,10 +623,9 @@ describe('Dashboard edit', () => {
beforeEach(() => {
cy.createSampleDashboards([0]);
openProperties();
selectColorScheme('supersetColors');
});
it('should accept a valid color scheme', () => {
it.skip('should accept a valid color scheme', () => {
openAdvancedProperties();
clearMetadata();
writeMetadata('{"color_scheme":"lyftColors"}');
@@ -1079,21 +636,21 @@ describe('Dashboard edit', () => {
applyChanges();
});
it('should overwrite the color scheme when advanced is closed', () => {
it.skip('should overwrite the color scheme when advanced is closed', () => {
selectColorScheme('blueToGreen');
openAdvancedProperties();
assertMetadata('blueToGreen');
applyChanges();
});
it('should overwrite the color scheme when advanced is open', () => {
it.skip('should overwrite the color scheme when advanced is open', () => {
openAdvancedProperties();
selectColorScheme('modernSunset');
assertMetadata('modernSunset');
applyChanges();
});
it('should not accept an invalid color scheme', () => {
it.skip('should not accept an invalid color scheme', () => {
openAdvancedProperties();
clearMetadata();
// allow console error
@@ -1159,7 +716,7 @@ describe('Dashboard edit', () => {
visitEdit();
});
it('should add charts', () => {
it.skip('should add charts', () => {
cy.get('[role="checkbox"]').click();
dragComponent();
cy.getBySel('dashboard-component-chart-holder').should('have.length', 1);
@@ -1208,7 +765,7 @@ describe('Dashboard edit', () => {
visitEdit();
});
it('should save', () => {
it.skip('should save', () => {
cy.get('[role="checkbox"]').click();
dragComponent();
cy.getBySel('header-save-button').should('be.enabled');

View File

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

View File

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

View File

@@ -99,13 +99,16 @@ describe('Color scheme control', () => {
cy.get('.ant-select-selection-item .color-scheme-label').trigger(
'mouseover',
);
cy.get('.color-scheme-tooltip').should('be.visible');
cy.get('.color-scheme-tooltip').contains('Superset Colors');
cy.get('.Control[data-test="color_scheme"]').scrollIntoView();
cy.get('.Control[data-test="color_scheme"] input[type="search"]').focus();
cy.focused().type('lyftColors');
cy.getBySel('lyftColors').should('exist');
cy.getBySel('lyftColors').trigger('mouseover');
cy.focused().type('lyftColors{enter}');
cy.get(
'.Control[data-test="color_scheme"] .ant-select-selection-item [data-test="lyftColors"]',
).should('exist');
cy.get('.ant-select-selection-item .color-scheme-label').trigger(
'mouseover',
);
cy.get('.color-scheme-tooltip').should('not.exist');
});
});

View File

@@ -140,7 +140,7 @@ export const sqlLabView = {
tabsNavList: "[class='ant-tabs-nav-list']",
tab: "[class='ant-tabs-tab-btn']",
addTabButton: dataTestLocator('add-tab-icon'),
tooltip: '.antd5-tooltip-content',
tooltip: '.ant-tooltip-content',
tabName: '.css-1suejie',
schemaInput: '[data-test=DatabaseSelector] > :nth-child(2)',
loadingIndicator: '.Select__loading-indicator',

View File

@@ -53067,6 +53067,14 @@
"node": ">=4"
}
},
"node_modules/viewport-mercator-project": {
"version": "6.2.3",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.0.0",
"gl-matrix": "^3.0.0"
}
},
"node_modules/vlq": {
"version": "0.2.3",
"license": "MIT"
@@ -57943,10 +57951,10 @@
"version": "0.18.25",
"license": "Apache-2.0",
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"prop-types": "^15.8.1",
"react-map-gl": "^6.1.19",
"supercluster": "^8.0.1"
"supercluster": "^8.0.1",
"viewport-mercator-project": "^6.1.1"
},
"peerDependencies": {
"@superset-ui/chart-controls": "*",
@@ -57955,30 +57963,6 @@
"react": "^15 || ^16"
}
},
"plugins/legacy-plugin-chart-map-box/node_modules/@math.gl/core": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@math.gl/core/-/core-4.1.0.tgz",
"integrity": "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==",
"license": "MIT",
"dependencies": {
"@math.gl/types": "4.1.0"
}
},
"plugins/legacy-plugin-chart-map-box/node_modules/@math.gl/types": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@math.gl/types/-/types-4.1.0.tgz",
"integrity": "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==",
"license": "MIT"
},
"plugins/legacy-plugin-chart-map-box/node_modules/@math.gl/web-mercator": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@math.gl/web-mercator/-/web-mercator-4.1.0.tgz",
"integrity": "sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw==",
"license": "MIT",
"dependencies": {
"@math.gl/core": "4.1.0"
}
},
"plugins/legacy-plugin-chart-map-box/node_modules/kdbush": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
@@ -68682,33 +68666,12 @@
"@superset-ui/legacy-plugin-chart-map-box": {
"version": "file:plugins/legacy-plugin-chart-map-box",
"requires": {
"@math.gl/web-mercator": "^4.1.0",
"prop-types": "^15.8.1",
"react-map-gl": "^6.1.19",
"supercluster": "^8.0.1"
"supercluster": "^8.0.1",
"viewport-mercator-project": "^6.1.1"
},
"dependencies": {
"@math.gl/core": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@math.gl/core/-/core-4.1.0.tgz",
"integrity": "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==",
"requires": {
"@math.gl/types": "4.1.0"
}
},
"@math.gl/types": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@math.gl/types/-/types-4.1.0.tgz",
"integrity": "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA=="
},
"@math.gl/web-mercator": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@math.gl/web-mercator/-/web-mercator-4.1.0.tgz",
"integrity": "sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw==",
"requires": {
"@math.gl/core": "4.1.0"
}
},
"kdbush": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
@@ -95424,6 +95387,13 @@
"unist-util-stringify-position": "^3.0.0"
}
},
"viewport-mercator-project": {
"version": "6.2.3",
"requires": {
"@babel/runtime": "^7.0.0",
"gl-matrix": "^3.0.0"
}
},
"vlq": {
"version": "0.2.3"
},

View File

@@ -18,8 +18,9 @@
*/
import { CSSProperties } from 'react';
import { kebabCase } from 'lodash';
import { TooltipPlacement } from 'antd/lib/tooltip';
import { t } from '@superset-ui/core';
import { Tooltip, TooltipProps, TooltipPlacement } from './Tooltip';
import { Tooltip, TooltipProps } from './Tooltip';
export interface InfoTooltipWithTriggerProps {
label?: string;

View File

@@ -17,41 +17,48 @@
* under the License.
*/
import { useTheme } from '@superset-ui/core';
import { Tooltip as BaseTooltip } from 'antd-v5';
import {
TooltipProps as BaseTooltipProps,
TooltipPlacement as BaseTooltipPlacement,
} from 'antd-v5/lib/tooltip';
import { useTheme, css } from '@superset-ui/core';
import { Tooltip as BaseTooltip } from 'antd';
import type { TooltipProps } from 'antd/lib/tooltip';
import { Global } from '@emotion/react';
export type TooltipProps = BaseTooltipProps;
export type TooltipPlacement = BaseTooltipPlacement;
export type { TooltipProps } from 'antd/lib/tooltip';
export const Tooltip = ({
overlayStyle,
color,
...props
}: BaseTooltipProps) => {
export const Tooltip = ({ overlayStyle, color, ...props }: TooltipProps) => {
const theme = useTheme();
const defaultColor = `${theme.colors.grayscale.dark2}e6`;
return (
<BaseTooltip
overlayStyle={{
fontSize: theme.typography.sizes.s,
lineHeight: '1.6',
maxWidth: theme.gridUnit * 62,
minWidth: theme.gridUnit * 30,
...overlayStyle,
}}
// make the tooltip display closer to the label
align={{ offset: [0, 1] }}
color={defaultColor || color}
trigger="hover"
placement="bottom"
// don't allow hovering over the tooltip
mouseLeaveDelay={0}
{...props}
/>
<>
{/* Safari hack to hide browser default tooltips */}
<Global
styles={css`
.ant-tooltip-open {
display: inline-block;
&::after {
content: '';
display: block;
}
}
`}
/>
<BaseTooltip
overlayStyle={{
fontSize: theme.typography.sizes.s,
lineHeight: '1.6',
maxWidth: theme.gridUnit * 62,
minWidth: theme.gridUnit * 30,
...overlayStyle,
}}
// make the tooltip display closer to the label
align={{ offset: [0, 1] }}
color={defaultColor || color}
trigger="hover"
placement="bottom"
// don't allow hovering over the tooltip
mouseLeaveDelay={0}
{...props}
/>
</>
);
};

View File

@@ -50,7 +50,6 @@ import {
import {
formatSelectOptions,
displayTimeRelatedControls,
getColorControlsProps,
D3_FORMAT_OPTIONS,
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
@@ -143,7 +142,9 @@ const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
renderTrigger: true,
schemes: () => sequentialSchemeRegistry.getMap(),
isLinear: true,
mapStateToProps: state => getColorControlsProps(state),
mapStateToProps: state => ({
dashboardId: state?.form_data?.dashboardId,
}),
};
const granularity: SharedControlConfig<'SelectControl'> = {
@@ -332,7 +333,9 @@ 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 => getColorControlsProps(state),
mapStateToProps: state => ({
dashboardId: state?.form_data?.dashboardId,
}),
};
const time_shift_color: SharedControlConfig<'CheckboxControl'> = {

View File

@@ -87,9 +87,6 @@ export interface Dataset {
}
export interface ControlPanelState {
slice: {
slice_id: number;
};
form_data: QueryFormData;
datasource: Dataset | QueryResponse | null;
controls: ControlStateMapping;
@@ -262,7 +259,6 @@ export interface BaseControlConfig<
props: ControlPanelsContainerProps,
controlData: AnyDict,
) => boolean;
disableStash?: boolean;
hidden?:
| boolean
| ((props: ControlPanelsContainerProps, controlData: AnyDict) => boolean);

View File

@@ -1,32 +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.
*/
export const getColorControlsProps = (state: Record<string, any>) => {
const dashboardId = state?.form_data?.dashboardId;
return {
chartId: state?.slice?.slice_id,
dashboardId,
hasDashboardColorScheme:
!!dashboardId && !!state?.form_data?.dashboard_color_scheme,
hasCustomLabelsColor:
Object.keys(state?.form_data?.label_colors || {}).length > 0,
colorNamespace: state?.form_data?.color_namespace,
mapLabelsColors: state?.form_data?.map_label_colors || {},
sharedLabelsColors: state?.form_data?.shared_label_colors || [],
};
};

View File

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

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { QueryObject, SqlaFormData } from '@superset-ui/core';
import { boxplotOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -27,7 +27,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
time_grain_sqla: 'P1Y',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: [

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { QueryObject, SqlaFormData } from '@superset-ui/core';
import { contributionOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -27,7 +27,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: [

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { QueryObject, SqlaFormData } from '@superset-ui/core';
import { flattenOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -27,7 +27,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: [

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { histogramOperator } from '@superset-ui/chart-controls';
import { SqlaFormData, VizType } from '@superset-ui/core';
import { SqlaFormData } from '@superset-ui/core';
import { omit } from 'lodash';
const formData: SqlaFormData = {
@@ -26,7 +26,7 @@ const formData: SqlaFormData = {
cumulative: true,
normalize: true,
groupby: ['country', 'region'],
viz_type: VizType.LegacyHistogram,
viz_type: 'histogram',
datasource: 'foo',
};

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { QueryObject, SqlaFormData } from '@superset-ui/core';
import { pivotOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -27,7 +27,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
show_empty_columns: true,
};
const queryObject: QueryObject = {

View File

@@ -16,12 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
DTTM_ALIAS,
QueryObject,
SqlaFormData,
VizType,
} from '@superset-ui/core';
import { DTTM_ALIAS, QueryObject, SqlaFormData } from '@superset-ui/core';
import { prophetOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -32,7 +27,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
time_grain_sqla: 'P1Y',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: [

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { QueryObject, SqlaFormData } from '@superset-ui/core';
import { rankOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -26,7 +26,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
truncate_metric: true,
};
const queryObject: QueryObject = {

View File

@@ -16,12 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
ComparisonType,
QueryObject,
SqlaFormData,
VizType,
} from '@superset-ui/core';
import { ComparisonType, QueryObject, SqlaFormData } from '@superset-ui/core';
import { renameOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -31,7 +26,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
truncate_metric: true,
};
const queryObject: QueryObject = {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { QueryObject, SqlaFormData } from '@superset-ui/core';
import { resampleOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -27,7 +27,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: [

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { QueryObject, SqlaFormData } from '@superset-ui/core';
import { rollingWindowOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -27,7 +27,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: [

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { QueryObject, SqlaFormData } from '@superset-ui/core';
import { sortOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -27,7 +27,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: [

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { QueryObject, SqlaFormData } from '@superset-ui/core';
import { timeCompareOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
@@ -27,7 +27,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: [

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObject, SqlaFormData, VizType } from '@superset-ui/core';
import { QueryObject, SqlaFormData } from '@superset-ui/core';
import {
timeCompareOperator,
timeComparePivotOperator,
@@ -30,7 +30,7 @@ const formData: SqlaFormData = {
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
show_empty_columns: true,
};
const queryObject: QueryObject = {

View File

@@ -16,12 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryFormData, QueryFormMetric, VizType } from '@superset-ui/core';
import { QueryFormData, QueryFormMetric } from '@superset-ui/core';
import { extractExtraMetrics } from '@superset-ui/chart-controls';
const baseFormData: QueryFormData = {
datasource: 'dummy',
viz_type: VizType.Table,
viz_type: 'table',
metrics: ['a', 'b'],
columns: ['foo', 'bar'],
limit: 100,

View File

@@ -17,11 +17,11 @@
* under the License.
*/
import { isDerivedSeries } from '@superset-ui/chart-controls';
import { SqlaFormData, ComparisonType, VizType } from '@superset-ui/core';
import { SqlaFormData, ComparisonType } from '@superset-ui/core';
const formData: SqlaFormData = {
datasource: 'foo',
viz_type: VizType.Table,
viz_type: 'table',
};
const series = {
id: 'metric__1 month ago',

View File

@@ -1,112 +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 { getColorControlsProps } from '../../src';
describe('getColorControlsProps', () => {
it('should return default values when state is empty', () => {
const state = {};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: undefined,
dashboardId: undefined,
hasDashboardColorScheme: false,
hasCustomLabelsColor: false,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
it('should return correct values when state has form_data with dashboardId and color scheme', () => {
const state = {
form_data: {
dashboardId: 123,
dashboard_color_scheme: 'blueScheme',
label_colors: {},
},
slice: { slice_id: 456 },
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: 456,
dashboardId: 123,
hasDashboardColorScheme: true,
hasCustomLabelsColor: false,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
it('should detect custom label colors correctly', () => {
const state = {
form_data: {
dashboardId: 123,
label_colors: { label1: '#000000' },
},
slice: { slice_id: 456 },
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: 456,
dashboardId: 123,
hasDashboardColorScheme: false,
hasCustomLabelsColor: true,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
it('should return shared label colors when available', () => {
const state = {
form_data: {
shared_label_colors: ['#FF5733', '#33FF57'],
},
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: undefined,
dashboardId: undefined,
hasDashboardColorScheme: false,
hasCustomLabelsColor: false,
sharedLabelsColors: ['#FF5733', '#33FF57'],
colorNamespace: undefined,
mapLabelsColors: {},
});
});
it('should handle missing form_data and slice properties', () => {
const state = {
form_data: {
dashboardId: 789,
},
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: undefined,
dashboardId: 789,
hasDashboardColorScheme: false,
hasCustomLabelsColor: false,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
});

View File

@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import { VizType } from '@superset-ui/core';
import { displayTimeRelatedControls } from '../../src';
const mockData = {
@@ -36,7 +35,7 @@ const mockData = {
exportState: {},
form_data: {
datasource: '22__table',
viz_type: VizType.Table,
viz_type: 'table',
},
};

View File

@@ -16,12 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryFormData, VizType } from '@superset-ui/core';
import { QueryFormData } from '@superset-ui/core';
import { getStandardizedControls } from '../../src';
const formData: QueryFormData = {
datasource: '30__table',
viz_type: VizType.Table,
viz_type: 'table',
standardizedFormData: {
controls: {
metrics: ['count(*)', 'sum(sales)'],
@@ -34,7 +34,7 @@ const formData: QueryFormData = {
test('without standardizedFormData', () => {
getStandardizedControls().setStandardizedControls({
datasource: '30__table',
viz_type: VizType.Table,
viz_type: 'table',
});
expect(getStandardizedControls().controls).toEqual({
metrics: [],

View File

@@ -41,7 +41,6 @@ export { default as ChartDataProvider } from './components/ChartDataProvider';
export * from './types/Base';
export * from './types/TransformFunction';
export * from './types/QueryResponse';
export * from './types/VizType';
export { default as __hack_reexport_chart_Base } from './types/Base';
export { default as __hack_reexport_chart_TransformFunction } from './types/TransformFunction';

View File

@@ -1,72 +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.
*/
export enum VizType {
Area = 'echarts_area',
Bar = 'echarts_timeseries_bar',
BigNumber = 'big_number',
BigNumberTotal = 'big_number_total',
BigNumberPeriodOverPeriod = 'pop_kpi',
BoxPlot = 'box_plot',
Bubble = 'bubble_v2',
Bullet = 'bullet',
Calendar = 'cal_heatmap',
Chord = 'chord',
Compare = 'compare',
CountryMap = 'country_map',
DistBar = 'dist_bar',
EventFlow = 'event_flow',
Funnel = 'funnel',
Gauge = 'gauge_chart',
Graph = 'graph_chart',
Handlebars = 'handlebars',
Heatmap = 'heatmap_v2',
Histogram = 'histogram_v2',
Horizon = 'horizon',
LegacyArea = 'area',
LegacyBar = 'bar',
LegacyBubble = 'bubble',
LegacyHeatmap = 'heatmap',
LegacyHistogram = 'histogram',
LegacyLine = 'line',
LegacySankey = 'sankey',
Line = 'echarts_timeseries_line',
MapBox = 'mapbox',
MixedTimeseries = 'mixed_timeseries',
PairedTTest = 'paired_ttest',
ParallelCoordinates = 'para',
Partition = 'partition',
Pie = 'pie',
PivotTable = 'pivot_table_v2',
Radar = 'radar',
Rose = 'rose',
Sankey = 'sankey_v2',
Scatter = 'echarts_timeseries_scatter',
SmoothLine = 'echarts_timeseries_smooth',
Step = 'echarts_timeseries_step',
Sunburst = 'sunburst_v2',
Table = 'table',
TimePivot = 'time_pivot',
TimeTable = 'time_table',
Timeseries = 'echarts_timeseries',
Tree = 'tree_chart',
Treemap = 'treemap_v2',
Waterfall = 'waterfall',
WordCloud = 'word_cloud',
WorldMap = 'world_map',
}

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import { cloneDeep } from 'lodash';
import CategoricalColorScale from './CategoricalColorScale';
import { ColorsLookup } from './types';
import getCategoricalSchemeRegistry from './CategoricalSchemeRegistrySingleton';
@@ -38,21 +37,10 @@ export default class CategoricalColorNamespace {
this.forcedItems = {};
}
/**
* 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() ?? '';
getScale(schemeId?: string) {
const id = schemeId ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
const scheme = getCategoricalSchemeRegistry().get(id);
return new CategoricalColorScale(
scheme?.colors ?? [],
this.forcedItems,
colorScheme,
);
return new CategoricalColorScale(scheme?.colors ?? [], this.forcedItems);
}
/**
@@ -71,17 +59,6 @@ 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: {
@@ -103,19 +80,16 @@ export function getNamespace(name: string = DEFAULT_NAMESPACE) {
export function getColor(
value?: string,
colorScheme?: string,
schemeId?: string,
namespace?: string,
) {
return getNamespace(namespace).getScale(colorScheme).getColor(value);
return getNamespace(namespace).getScale(schemeId).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(colorScheme?: string, namespace?: string) {
return getNamespace(namespace).getScale(colorScheme);
export function getScale(scheme?: string, namespace?: string) {
return getNamespace(namespace).getScale(scheme);
}

View File

@@ -21,16 +21,14 @@ import { scaleOrdinal, ScaleOrdinal } from 'd3-scale';
import { ExtensibleFunction } from '../models';
import { ColorsInitLookup, ColorsLookup } from './types';
import stringifyAndTrim from './stringifyAndTrim';
import getLabelsColorMap, {
LabelsColorMapSource,
} from './LabelsColorMapSingleton';
import getLabelsColorMap 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): string;
(x: { toString(): string }, y?: number, w?: string): string;
}
class CategoricalColorScale extends ExtensibleFunction {
@@ -52,16 +50,11 @@ class CategoricalColorScale extends ExtensibleFunction {
* Constructor
* @param {*} colors an array of colors
* @param {*} forcedColors optional parameter that comes from parent
* @param {*} appliedColorScheme the color scheme applied to the chart
*
* (usually CategoricalColorNamespace)
*/
constructor(
colors: string[],
forcedColors: ColorsInitLookup = {},
appliedColorScheme?: string,
) {
super((value: string, sliceId?: number) =>
this.getColor(value, sliceId, appliedColorScheme),
constructor(colors: string[], forcedColors: ColorsInitLookup = {}) {
super((value: string, sliceId?: number, colorScheme?: string) =>
this.getColor(value, sliceId, colorScheme),
);
// holds original color scheme colors
this.originColors = colors;
@@ -114,28 +107,15 @@ 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 appliedColorScheme the color scheme applied to the chart
* @param colorScheme the original color scheme of the chart
* @returns the color or the next available color
*/
getColor(
value?: string,
sliceId?: number,
appliedColorScheme?: string,
): string {
getColor(value?: string, sliceId?: number, colorScheme?: string): string {
const cleanedValue = stringifyAndTrim(value);
// priority: forced color (aka custom label colors) > shared color > scale color
// priority: forced color (i.e. custom label colors) > shared color > scale color
const forcedColor = this.forcedColors?.[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));
const isExistingLabel = this.chartLabelsColorMap.has(cleanedValue);
let color = forcedColor || this.scale(cleanedValue);
// a forced color will always be used independently of the usage count
if (!forcedColor && !isExistingLabel) {
@@ -148,7 +128,7 @@ class CategoricalColorScale extends ExtensibleFunction {
this.isColorUsed(color)
) {
// fallback to least used color
color = this.getNextAvailableColor(cleanedValue, color);
color = this.getNextAvailableColor(color);
}
}
@@ -161,7 +141,7 @@ class CategoricalColorScale extends ExtensibleFunction {
cleanedValue,
color,
sliceId,
appliedColorScheme,
colorScheme,
);
}
return color;
@@ -184,76 +164,48 @@ class CategoricalColorScale extends ExtensibleFunction {
* @param color the color to check
* @returns the count of the color usage in this slice
*/
getColorUsageCount(color: string): number {
return Array.from(this.chartLabelsColorMap.values()).filter(
value => value === color,
).length;
getColorUsageCount(currentColor: string): number {
let count = 0;
this.chartLabelsColorMap.forEach(color => {
if (color === currentColor) {
count += 1;
}
});
return count;
}
/**
* Lower chances of color collision by returning the least used color.
* Checks across colors of current slice within chartLabelsColorMap.
* Lower chances of color collision by returning the least used color
* Checks across colors of current slice within LabelsColorMapSingleton
*
* @param currentLabel the current label
* @param currentColor the current color
* @returns the least used color that is not the current color
* @returns the least used color that is not the excluded color
*/
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)]),
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,
);
// 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;
// fallback to currentColor color
if (!otherColors.length || hasNoneAvailable) {
return currentColor;
}
// If all colors are used, calculate scores and choose the best one
const otherColors = this.colors.filter(color => color !== currentColor);
// Finding the least used color
const leastUsedColor = otherColors.reduce((min, entry) =>
entry.count < min.count ? entry : min,
).color;
// 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);
return leastUsedColor;
}
/**

View File

@@ -18,7 +18,6 @@
*/
import { makeSingleton } from '../utils';
import CategoricalColorNamespace from './CategoricalColorNamespace';
export enum LabelsColorMapSource {
Dashboard,
@@ -26,10 +25,7 @@ export enum LabelsColorMapSource {
}
export class LabelsColorMap {
chartsLabelsMap: Map<
number,
{ labels: string[]; scheme?: string; ownScheme?: string }
>;
chartsLabelsMap: Map<number, { labels: string[]; scheme?: string }>;
colorMap: Map<string, string>;
@@ -42,38 +38,17 @@ export class LabelsColorMap {
this.source = LabelsColorMapSource.Dashboard;
}
/**
* 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();
}
updateColorMap(categoricalNamespace: any, colorScheme?: string) {
const newColorMap = new Map();
this.colorMap.clear();
this.chartsLabelsMap.forEach((chartConfig, sliceId) => {
const { labels, ownScheme } = chartConfig;
const appliedColorScheme = colorScheme || ownScheme;
const colorScale = categoricalNamespace.getScale(appliedColorScheme);
const { labels, scheme: originalChartColorScheme } = chartConfig;
const currentColorScheme = colorScheme || originalChartColorScheme;
const colorScale = categoricalNamespace.getScale(currentColorScheme);
labels.forEach(label => {
// 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);
}
const newColor = colorScale.getColor(label, sliceId);
newColorMap.set(label, newColor);
});
});
this.colorMap = newColorMap;
@@ -83,63 +58,29 @@ 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: undefined,
ownScheme: undefined,
scheme: '',
};
const { labels } = chartConfig;
if (!labels.includes(label)) {
labels.push(label);
this.chartsLabelsMap.set(sliceId, {
labels,
scheme: colorScheme,
ownScheme: chartConfig.ownScheme,
});
}
if (this.source === LabelsColorMapSource.Dashboard) {
this.colorMap.set(label, color);
}
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;
@@ -155,19 +96,9 @@ export class LabelsColorMap {
this.colorMap = newColorMap;
}
/**
* Clear the shared labels color map.
*/
clear() {
this.colorMap.clear();
}
/**
* Clears all maps
*/
reset() {
this.clear();
this.chartsLabelsMap.clear();
this.colorMap.clear();
}
}

View File

@@ -28,7 +28,6 @@ import {
getChartBuildQueryRegistry,
getChartMetadataRegistry,
ChartMetadata,
VizType,
} from '@superset-ui/core';
import { LOGIN_GLOB } from '../fixtures/constants';
@@ -87,13 +86,13 @@ describe('ChartClient', () => {
sliceId,
formData: {
granularity: 'second',
viz_type: VizType.LegacyBar,
viz_type: 'bar',
},
}),
).resolves.toEqual({
...sankeyFormData,
granularity: 'second',
viz_type: VizType.LegacyBar,
viz_type: 'bar',
});
});
it('returns promise of formData if only formData was given', () =>
@@ -102,13 +101,13 @@ describe('ChartClient', () => {
formData: {
datasource: '1__table',
granularity: 'minute',
viz_type: VizType.LegacyLine,
viz_type: 'line',
},
}),
).resolves.toEqual({
datasource: '1__table',
granularity: 'minute',
viz_type: VizType.LegacyLine,
viz_type: 'line',
}));
it('rejects if none of sliceId or formData is specified', () =>
expect(
@@ -121,12 +120,12 @@ describe('ChartClient', () => {
describe('.loadQueryData(formData, options)', () => {
it('returns a promise of query data for known chart type', () => {
getChartMetadataRegistry().registerValue(
VizType.WordCloud,
'word_cloud',
new ChartMetadata({ name: 'Word Cloud', thumbnail: '' }),
);
getChartBuildQueryRegistry().registerValue(
VizType.WordCloud,
'word_cloud',
(formData: QueryFormData) => buildQueryContext(formData),
);
fetchMock.post('glob:*/api/v1/chart/data', [
@@ -139,7 +138,7 @@ describe('ChartClient', () => {
return expect(
chartClient.loadQueryData({
granularity: 'minute',
viz_type: VizType.WordCloud,
viz_type: 'word_cloud',
datasource: '1__table',
}),
).resolves.toEqual([
@@ -256,7 +255,7 @@ describe('ChartClient', () => {
it('loadAllDataNecessaryForAChart', () => {
fetchMock.get(`glob:*/api/v1/form_data/?slice_id=${sliceId}`, {
granularity: 'minute',
viz_type: VizType.LegacyLine,
viz_type: 'line',
datasource: '1__table',
color: 'living-coral',
});
@@ -276,12 +275,12 @@ describe('ChartClient', () => {
});
getChartMetadataRegistry().registerValue(
VizType.LegacyLine,
'line',
new ChartMetadata({ name: 'Line', thumbnail: '.gif' }),
);
getChartBuildQueryRegistry().registerValue(
VizType.LegacyLine,
'line',
(formData: QueryFormData) => buildQueryContext(formData),
);
@@ -297,7 +296,7 @@ describe('ChartClient', () => {
},
formData: {
granularity: 'minute',
viz_type: VizType.LegacyLine,
viz_type: 'line',
datasource: '1__table',
color: 'living-coral',
},

View File

@@ -19,11 +19,11 @@
/* eslint sort-keys: 'off' */
/** The form data defined here is based on default visualizations packaged with Apache Superset */
import { TimeGranularity, VizType } from '@superset-ui/core';
import { TimeGranularity } from '@superset-ui/core';
export const bigNumberFormData = {
datasource: '3__table',
viz_type: VizType.BigNumber,
viz_type: 'big_number',
slice_id: 54,
granularity_sqla: 'ds',
time_grain_sqla: TimeGranularity.DAY,
@@ -39,7 +39,7 @@ export const bigNumberFormData = {
export const wordCloudFormData = {
datasource: '3__table',
viz_type: VizType.WordCloud,
viz_type: 'word_cloud',
slice_id: 60,
url_params: {},
granularity_sqla: 'ds',
@@ -56,7 +56,7 @@ export const wordCloudFormData = {
export const sunburstFormData = {
datasource: '2__table',
viz_type: VizType.Sunburst,
viz_type: 'sunburst_v2',
slice_id: 47,
url_params: {},
granularity_sqla: 'year',
@@ -71,7 +71,7 @@ export const sunburstFormData = {
export const sankeyFormData = {
datasource: '1__table',
viz_type: VizType.LegacySankey,
viz_type: 'sankey',
slice_id: 1,
url_params: {},
granularity_sqla: null,

View File

@@ -31,7 +31,6 @@ import {
QueryFormData,
DatasourceType,
supersetTheme,
VizType,
} from '@superset-ui/core';
describe('ChartPlugin', () => {
@@ -60,7 +59,7 @@ describe('ChartPlugin', () => {
const FORM_DATA = {
datasource: '1__table',
granularity: 'day',
viz_type: VizType.Table,
viz_type: 'table',
};
it('creates a new plugin', () => {

View File

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

View File

@@ -18,11 +18,7 @@
*/
import { ScaleOrdinal } from 'd3-scale';
import {
CategoricalColorScale,
FeatureFlag,
LabelsColorMapSource,
} from '@superset-ui/core';
import { CategoricalColorScale, FeatureFlag } from '@superset-ui/core';
describe('CategoricalColorScale', () => {
beforeEach(() => {
@@ -47,6 +43,7 @@ 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(
@@ -70,7 +67,7 @@ describe('CategoricalColorScale', () => {
>;
let getNextAvailableColorSpy: jest.SpyInstance<
string,
[currentLabel: string, currentColor: string]
[currentColor: string]
>;
beforeEach(() => {
@@ -86,36 +83,6 @@ 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',
@@ -210,10 +177,7 @@ describe('CategoricalColorScale', () => {
scale.getColor('testValue3');
scale.getColor('testValue4');
expect(getNextAvailableColorSpy).toHaveBeenCalledWith(
'testValue4',
'blue',
);
expect(getNextAvailableColorSpy).toHaveBeenCalledWith('blue');
getNextAvailableColorSpy.mockClear();
@@ -325,25 +289,23 @@ describe('CategoricalColorScale', () => {
});
});
describe('.getNextAvailableColor(currentLabel, currentColor)', () => {
describe('.getNextAvailableColor(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('fish', 'blue')).toBe('green');
expect(scale.getNextAvailableColor('blue')).toBe('green');
});
it('returns the least used color among all', () => {
it('handles cases where all colors are equally used and returns the current color', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
scale.getColor('fish'); // green
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');
// All colors used once, so the function should return the current color
expect(scale.getNextAvailableColor('red')).toBe('red');
});
it('returns the least used color accurately even when some colors are used more frequently', () => {
@@ -362,57 +324,7 @@ describe('CategoricalColorScale', () => {
scale.getColor('pony'); // green
// Yellow is the least used color, so it should be returned.
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');
expect(scale.getNextAvailableColor('blue')).toBe('yellow');
});
});

View File

@@ -53,7 +53,7 @@ describe('LabelsColorMap', () => {
beforeEach(() => {
getLabelsColorMap().source = LabelsColorMapSource.Dashboard;
getLabelsColorMap().reset();
getLabelsColorMap().clear();
});
it('has default value out-of-the-box', () => {
@@ -92,17 +92,11 @@ describe('LabelsColorMap', () => {
expect(Object.fromEntries(colorMap)).toEqual({ b: 'green' });
});
it('should set a new color only when source is dashboard', () => {
it('should do nothing when source is not dashboard', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Explore;
labelsColorMap.addSlice('a', 'red', 1);
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' });
expect(Object.fromEntries(labelsColorMap.chartsLabelsMap)).toEqual({});
});
});
@@ -132,7 +126,7 @@ describe('LabelsColorMap', () => {
});
});
describe('.updateColorMap(namespace, scheme, merge)', () => {
describe('.updateColorMap(namespace, scheme)', () => {
let categoricalNamespace: any;
let mockedNamespace: any;
let labelsColorMap: any;
@@ -147,24 +141,18 @@ 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);
@@ -193,23 +181,6 @@ 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,
@@ -260,47 +231,4 @@ describe('LabelsColorMap', () => {
expect(Object.fromEntries(colorMap)).toEqual({});
});
});
describe('setOwnColorScheme(sliceId, ownScheme)', () => {
it('should update the scheme in the config', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Explore;
const sliceId = 1;
const initialConfig = { labels: ['initial config'] };
labelsColorMap.chartsLabelsMap = new Map();
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
...initialConfig,
ownScheme: 'newScheme',
});
});
it('should update ownScheme when source is not Explore', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Dashboard;
const sliceId = 1;
const initialConfig = { labels: ['initial config'] };
labelsColorMap.chartsLabelsMap = new Map();
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
...initialConfig,
ownScheme: 'newScheme',
});
});
it('should do nothing when chart config does not exist', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Explore;
const sliceId = 1;
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual(undefined);
});
});
});

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
import { VizType } from '@superset-ui/core';
import { getFormData } from '../../../../src/query/api/legacy';
import setupClientForTest from '../setupClientForTest';
@@ -29,7 +28,7 @@ describe('getFormData()', () => {
const mockData = {
datasource: '1__table',
viz_type: VizType.LegacySankey,
viz_type: 'sankey',
slice_id: 1,
url_params: {},
granularity_sqla: null,

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
import { buildQueryContext, ApiV1, VizType } from '@superset-ui/core';
import { buildQueryContext, ApiV1 } from '@superset-ui/core';
import setupClientForTest from '../setupClientForTest';
describe('API v1 > getChartData()', () => {
@@ -39,7 +39,7 @@ describe('API v1 > getChartData()', () => {
const result = await ApiV1.getChartData(
buildQueryContext({
granularity: 'minute',
viz_type: VizType.WordCloud,
viz_type: 'word_cloud',
datasource: '1__table',
}),
);

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { buildQueryContext, VizType } from '@superset-ui/core';
import { buildQueryContext } from '@superset-ui/core';
import * as queryModule from '../../src/query/normalizeTimeColumn';
describe('buildQueryContext', () => {
@@ -24,7 +24,7 @@ describe('buildQueryContext', () => {
const queryContext = buildQueryContext({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
});
expect(queryContext.datasource.id).toBe(5);
expect(queryContext.datasource.type).toBe('table');
@@ -37,7 +37,7 @@ describe('buildQueryContext', () => {
{
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
source: 'source_column',
source_category: 'source_category_column',
target: 'target_column',
@@ -75,7 +75,7 @@ describe('buildQueryContext', () => {
{
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
source: 'source_column',
source_category: 'source_category_column',
target: 'target_column',
@@ -103,7 +103,7 @@ describe('buildQueryContext', () => {
const queryContext = buildQueryContext(
{
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
},
() => [
{
@@ -133,7 +133,7 @@ describe('buildQueryContext', () => {
buildQueryContext(
{
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
x_axis: 'axis',
},
() => [{}],

View File

@@ -25,7 +25,6 @@ import {
AnnotationType,
buildQueryObject,
QueryObject,
VizType,
} from '@superset-ui/core';
describe('buildQueryObject', () => {
@@ -35,7 +34,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
});
expect(query.granularity).toEqual('ds');
});
@@ -44,7 +43,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
metric: 'sum__num',
secondary_metric: 'avg__num',
});
@@ -55,7 +54,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
extra_filters: [{ col: 'abc', op: '==', val: 'qwerty' }],
adhoc_filters: [
{
@@ -89,7 +88,7 @@ describe('buildQueryObject', () => {
{
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
my_custom_metric_control: 'sum__num',
},
{ my_custom_metric_control: 'metrics' },
@@ -102,7 +101,7 @@ describe('buildQueryObject', () => {
{
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
metrics: ['sum__num'],
my_custom_metric_control: 'avg__num',
},
@@ -116,7 +115,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
limit: series_limit,
});
expect(query.series_limit).toEqual(series_limit);
@@ -127,7 +126,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
series_limit,
});
expect(query.series_limit).toEqual(series_limit);
@@ -138,7 +137,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
order_desc: orderDesc,
});
expect(query.order_desc).toEqual(orderDesc);
@@ -149,7 +148,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
timeseries_limit_metric: metric,
});
expect(query.series_limit_metric).toEqual(metric);
@@ -160,7 +159,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.PivotTable,
viz_type: 'pivot_table_v2',
series_limit_metric: metric,
});
expect(query.series_limit_metric).toEqual(metric);
@@ -171,7 +170,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.PivotTable,
viz_type: 'pivot_table_v2',
series_limit_metric: metric,
});
expect(query.series_limit_metric).toEqual(undefined);
@@ -181,7 +180,7 @@ describe('buildQueryObject', () => {
const baseQuery = {
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
row_limit: null,
};
@@ -268,7 +267,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
annotation_layers: annotationLayers,
});
expect(query.annotation_layers).toEqual(annotationLayers);
@@ -279,7 +278,7 @@ describe('buildQueryObject', () => {
buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
url_params: { abc: '123' },
}).url_params,
).toEqual({ abc: '123' });
@@ -287,7 +286,7 @@ describe('buildQueryObject', () => {
buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
// @ts-expect-error
url_params: null,
}).url_params,
@@ -299,7 +298,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity,
viz_type: VizType.Table,
viz_type: 'table',
});
expect(query.granularity).toEqual(granularity);
});
@@ -309,7 +308,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: granularity,
viz_type: VizType.Table,
viz_type: 'table',
});
expect(query.granularity).toEqual(granularity);
});
@@ -321,7 +320,7 @@ describe('buildQueryObject', () => {
query = buildQueryObject({
datasource: '5__table',
granularity_sqla: 'ds',
viz_type: VizType.Table,
viz_type: 'table',
custom_params: customParams,
});
expect(query.custom_params).toEqual(customParams);

View File

@@ -16,13 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { isXAxisSet, VizType } from '@superset-ui/core';
import { isXAxisSet } from '@superset-ui/core';
test('isXAxisSet', () => {
expect(isXAxisSet({ datasource: '123', viz_type: 'table' })).not.toBeTruthy();
expect(
isXAxisSet({ datasource: '123', viz_type: VizType.Table }),
).not.toBeTruthy();
expect(
isXAxisSet({ datasource: '123', viz_type: VizType.Table, x_axis: 'axis' }),
isXAxisSet({ datasource: '123', viz_type: 'table', x_axis: 'axis' }),
).toBeTruthy();
});

View File

@@ -16,13 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { normalizeOrderBy, QueryObject, VizType } from '@superset-ui/core';
import { normalizeOrderBy, QueryObject } from '@superset-ui/core';
describe('normalizeOrderBy', () => {
it('should not change original queryObject when orderby populated', () => {
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
orderby: [['count(*)', true]],
};
@@ -32,7 +32,7 @@ describe('normalizeOrderBy', () => {
it('has series_limit_metric in queryObject', () => {
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
metrics: ['count(*)'],
series_limit_metric: {
@@ -50,7 +50,7 @@ describe('normalizeOrderBy', () => {
expect(expectedQueryObject).not.toHaveProperty('order_desc');
expect(expectedQueryObject).toEqual({
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
metrics: ['count(*)'],
orderby: [
@@ -72,7 +72,7 @@ describe('normalizeOrderBy', () => {
it('should transform legacy_order_by in queryObject', () => {
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
metrics: ['count(*)'],
legacy_order_by: {
@@ -90,7 +90,7 @@ describe('normalizeOrderBy', () => {
expect(expectedQueryObject).not.toHaveProperty('order_desc');
expect(expectedQueryObject).toEqual({
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
metrics: ['count(*)'],
orderby: [
@@ -112,7 +112,7 @@ describe('normalizeOrderBy', () => {
it('has metrics in queryObject', () => {
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
metrics: ['count(*)'],
order_desc: true,
@@ -122,7 +122,7 @@ describe('normalizeOrderBy', () => {
expect(expectedQueryObject).not.toHaveProperty('order_desc');
expect(expectedQueryObject).toEqual({
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
metrics: ['count(*)'],
orderby: [['count(*)', false]],
@@ -132,7 +132,7 @@ describe('normalizeOrderBy', () => {
it('should not change', () => {
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
};
expect(normalizeOrderBy(query)).toEqual(query);
@@ -141,7 +141,7 @@ describe('normalizeOrderBy', () => {
it('remove empty orderby', () => {
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
orderby: [],
};
@@ -151,7 +151,7 @@ describe('normalizeOrderBy', () => {
it('remove orderby with an empty array', () => {
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
orderby: [[]],
};
@@ -161,7 +161,7 @@ describe('normalizeOrderBy', () => {
it('remove orderby with an empty metric', () => {
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
orderby: [['', true]],
};
@@ -171,7 +171,7 @@ describe('normalizeOrderBy', () => {
it('remove orderby with an empty adhoc metric', () => {
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
orderby: [[{}, true]],
};
@@ -181,7 +181,7 @@ describe('normalizeOrderBy', () => {
it('remove orderby with an non-boolean type', () => {
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
time_range: '1 year ago : 2013',
// @ts-ignore
orderby: [['count(*)', 'true']],

View File

@@ -20,13 +20,12 @@ import {
normalizeTimeColumn,
QueryObject,
SqlaFormData,
VizType,
} from '@superset-ui/core';
test('should return original QueryObject if x_axis is empty', () => {
const formData: SqlaFormData = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
granularity: 'time_column',
time_grain_sqla: 'P1Y',
time_range: '1 year ago : 2013',
@@ -35,7 +34,7 @@ test('should return original QueryObject if x_axis is empty', () => {
};
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
granularity: 'time_column',
extras: {
time_grain_sqla: 'P1Y',
@@ -52,7 +51,7 @@ test('should return original QueryObject if x_axis is empty', () => {
test('should support different columns for x-axis and granularity', () => {
const formData: SqlaFormData = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
granularity: 'time_column',
time_grain_sqla: 'P1Y',
time_range: '1 year ago : 2013',
@@ -62,7 +61,7 @@ test('should support different columns for x-axis and granularity', () => {
};
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
granularity: 'time_column',
extras: {
time_grain_sqla: 'P1Y',
@@ -77,7 +76,7 @@ test('should support different columns for x-axis and granularity', () => {
};
expect(normalizeTimeColumn(formData, query)).toEqual({
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
granularity: 'time_column',
extras: { where: '', having: '', time_grain_sqla: 'P1Y' },
time_range: '1 year ago : 2013',
@@ -99,7 +98,7 @@ test('should support different columns for x-axis and granularity', () => {
test('should support custom SQL in x-axis', () => {
const formData: SqlaFormData = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
granularity: 'time_column',
time_grain_sqla: 'P1Y',
time_range: '1 year ago : 2013',
@@ -113,7 +112,7 @@ test('should support custom SQL in x-axis', () => {
};
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
granularity: 'time_column',
extras: {
time_grain_sqla: 'P1Y',
@@ -135,7 +134,7 @@ test('should support custom SQL in x-axis', () => {
};
expect(normalizeTimeColumn(formData, query)).toEqual({
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
granularity: 'time_column',
extras: { where: '', having: '', time_grain_sqla: 'P1Y' },
time_range: '1 year ago : 2013',
@@ -157,7 +156,7 @@ test('should support custom SQL in x-axis', () => {
test('fallback and invalid columns value', () => {
const formData: SqlaFormData = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
granularity: 'time_column',
time_grain_sqla: 'P1Y',
time_range: '1 year ago : 2013',
@@ -171,7 +170,7 @@ test('fallback and invalid columns value', () => {
};
const query: QueryObject = {
datasource: '5__table',
viz_type: VizType.Table,
viz_type: 'table',
granularity: 'time_column',
extras: {
time_grain_sqla: 'P1Y',

View File

@@ -17,11 +17,11 @@
* under the License.
*/
import { getComparisonFilters, VizType } from '@superset-ui/core';
import { getComparisonFilters } from '@superset-ui/core';
const form_data = {
datasource: '22__table',
viz_type: VizType.BigNumberPeriodOverPeriod,
viz_type: 'pop_kpi',
slice_id: 97,
url_params: {
form_data_key:

View File

@@ -17,15 +17,11 @@
* under the License.
*/
import {
getComparisonInfo,
ComparisonTimeRangeType,
VizType,
} from '@superset-ui/core';
import { getComparisonInfo, ComparisonTimeRangeType } from '@superset-ui/core';
const form_data = {
datasource: '22__table',
viz_type: VizType.BigNumberPeriodOverPeriod,
viz_type: 'pop_kpi',
slice_id: 97,
url_params: {
form_data_key:

View File

@@ -17,12 +17,12 @@
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import ChordChartPlugin from '@superset-ui/legacy-plugin-chart-chord';
import data from './data';
import { withResizableChartDemo } from '../../../shared/components/ResizableChartDemo';
new ChordChartPlugin().configure({ key: VizType.Chord }).register();
new ChordChartPlugin().configure({ key: 'chord' }).register();
export default {
title: 'Legacy Chart Plugins/legacy-plugin-chart-chord',
@@ -31,7 +31,7 @@ export default {
export const basic = ({ width, height }) => (
<SuperChart
chartType={VizType.Chord}
chartType="chord"
width={width}
height={height}
queriesData={[{ data }]}

View File

@@ -17,12 +17,12 @@
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import HeatmapChartPlugin from '@superset-ui/legacy-plugin-chart-heatmap';
import ResizableChartDemo from '../../../shared/components/ResizableChartDemo';
import data from './data';
new HeatmapChartPlugin().configure({ key: VizType.LegacyHeatmap }).register();
new HeatmapChartPlugin().configure({ key: 'heatmap' }).register();
export default {
title: 'Legacy Chart Plugins/legacy-plugin-chart-heatmap',
@@ -30,7 +30,7 @@ export default {
export const basic = () => (
<SuperChart
chartType={VizType.LegacyHeatmap}
chartType="heatmap"
width={500}
height={500}
formData={{
@@ -67,7 +67,7 @@ export const resizable = () => (
<ResizableChartDemo>
{({ width, height }) => (
<SuperChart
chartType={VizType.LegacyHeatmap}
chartType="heatmap"
width={width}
height={height}
formData={{
@@ -104,7 +104,7 @@ export const resizable = () => (
export const withNullData = () => (
<SuperChart
chartType={VizType.LegacyHeatmap}
chartType="heatmap"
width={500}
height={500}
formData={{

View File

@@ -18,13 +18,11 @@
*/
/* eslint-disable no-magic-numbers */
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import HistogramChartPlugin from '@superset-ui/legacy-plugin-chart-histogram';
import data from './data';
new HistogramChartPlugin()
.configure({ key: VizType.LegacyHistogram })
.register();
new HistogramChartPlugin().configure({ key: 'histogram' }).register();
export default {
title: 'Legacy Chart Plugins/legacy-plugin-chart-histogram',
@@ -32,7 +30,7 @@ export default {
export const basic = () => (
<SuperChart
chartType={VizType.LegacyHistogram}
chartType="histogram"
width={400}
height={400}
queriesData={[{ data }]}

View File

@@ -17,11 +17,11 @@
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import HorizonChartPlugin from '@superset-ui/legacy-plugin-chart-horizon';
import data from './data';
new HorizonChartPlugin().configure({ key: VizType.Horizon }).register();
new HorizonChartPlugin().configure({ key: 'horizon' }).register();
export default {
title: 'Legacy Chart Plugins/legacy-plugin-chart-horizon',
@@ -29,7 +29,7 @@ export default {
export const basic = () => (
<SuperChart
chartType={VizType.Horizon}
chartType="horizon"
width={400}
height={400}
queriesData={[{ data }]}

View File

@@ -17,12 +17,12 @@
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import PartitionChartPlugin from '@superset-ui/legacy-plugin-chart-partition';
import data from './data';
import dummyDatasource from '../../../shared/dummyDatasource';
new PartitionChartPlugin().configure({ key: VizType.Partition }).register();
new PartitionChartPlugin().configure({ key: 'partition' }).register();
export default {
title: 'Legacy Chart Plugins/legacy-plugin-chart-partition',
@@ -30,7 +30,7 @@ export default {
export const basic = () => (
<SuperChart
chartType={VizType.Partition}
chartType="partition"
width={400}
height={400}
datasource={dummyDatasource}

View File

@@ -18,11 +18,11 @@
*/
/* eslint-disable no-magic-numbers, sort-keys */
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import RoseChartPlugin from '@superset-ui/legacy-plugin-chart-rose';
import data from './data';
new RoseChartPlugin().configure({ key: VizType.Rose }).register();
new RoseChartPlugin().configure({ key: 'rose' }).register();
export default {
title: 'Legacy Chart Plugins/legacy-plugin-chart-rose',
@@ -30,7 +30,7 @@ export default {
export const basic = () => (
<SuperChart
chartType={VizType.Rose}
chartType="rose"
width={400}
height={400}
queriesData={[{ data }]}

View File

@@ -18,12 +18,12 @@
*/
/* eslint-disable no-magic-numbers */
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import SankeyChartPlugin from '@superset-ui/legacy-plugin-chart-sankey';
import ResizableChartDemo from '../../../shared/components/ResizableChartDemo';
import data from './data';
new SankeyChartPlugin().configure({ key: VizType.LegacySankey }).register();
new SankeyChartPlugin().configure({ key: 'sankey' }).register();
export default {
title: 'Legacy Chart Plugins/legacy-plugin-chart-sankey',
@@ -31,7 +31,7 @@ export default {
export const basic = () => (
<SuperChart
chartType={VizType.LegacySankey}
chartType="sankey"
width={400}
height={400}
queriesData={[{ data }]}
@@ -45,7 +45,7 @@ export const resizable = () => (
<ResizableChartDemo>
{({ width, height }) => (
<SuperChart
chartType={VizType.LegacySankey}
chartType="sankey"
width={width}
height={height}
queriesData={[{ data }]}

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import { BigNumberChartPlugin } from '@superset-ui/plugin-chart-echarts';
import testData from './data';
@@ -37,7 +37,7 @@ const formData = {
showTrendLine: true,
startYAxisAtZero: true,
timeGrainSqla: 'P1Y',
vizType: VizType.BigNumber,
vizType: 'big_number',
yAxisFormat: '.3s',
};

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import { BigNumberTotalChartPlugin } from '@superset-ui/plugin-chart-echarts';
import data from './data';
@@ -37,7 +37,7 @@ export const totalBasic = () => (
formData={{
metric: 'sum__num',
subheader: 'total female participants',
vizType: VizType.BigNumberTotal,
vizType: 'big_number_total',
yAxisFormat: '.3s',
}}
/>
@@ -52,7 +52,7 @@ export const totalNoData = () => (
formData={{
metric: 'sum__num',
subheader: 'total female participants',
vizType: VizType.BigNumberTotal,
vizType: 'big_number_total',
yAxisFormat: '.3s',
}}
/>

View File

@@ -17,10 +17,9 @@
* under the License.
*/
import { VizType } from '@superset-ui/core';
import { AreaChartPlugin } from '@superset-ui/legacy-preset-chart-nvd3';
new AreaChartPlugin().configure({ key: VizType.LegacyArea }).register();
new AreaChartPlugin().configure({ key: 'area' }).register();
export default {
title: 'Legacy Chart Plugins/legacy-preset-chart-nvd3/Area',

View File

@@ -17,13 +17,13 @@
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import dummyDatasource from '../../../../../shared/dummyDatasource';
import data from '../data';
export const controlsShown = () => (
<SuperChart
chartType={VizType.LegacyArea}
chartType="area"
datasource={dummyDatasource}
width={400}
height={400}
@@ -40,7 +40,7 @@ export const controlsShown = () => (
showControls: true,
showLegend: true,
stackedStyle: 'stack',
vizType: VizType.LegacyArea,
vizType: 'area',
xAxisFormat: '%Y',
xAxisLabel: '',
xAxisShowminmax: false,

View File

@@ -17,13 +17,13 @@
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import dummyDatasource from '../../../../../shared/dummyDatasource';
import data from '../data';
export const expanded = () => (
<SuperChart
chartType={VizType.LegacyArea}
chartType="area"
datasource={dummyDatasource}
width={400}
height={400}
@@ -40,7 +40,7 @@ export const expanded = () => (
showControls: false,
showLegend: true,
stackedStyle: 'expand',
vizType: VizType.LegacyArea,
vizType: 'area',
xAxisFormat: '%Y',
xAxisLabel: '',
xAxisShowminmax: false,

View File

@@ -17,14 +17,14 @@
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import { SuperChart } from '@superset-ui/core';
import dummyDatasource from '../../../../../shared/dummyDatasource';
import data from '../data';
export const stacked = () => (
<SuperChart
id="stacked-area-chart"
chartType={VizType.LegacyArea}
chartType="area"
datasource={dummyDatasource}
width={400}
height={400}
@@ -41,7 +41,7 @@ export const stacked = () => (
showControls: false,
showLegend: true,
stackedStyle: 'stack',
vizType: VizType.LegacyArea,
vizType: 'area',
xAxisFormat: '%Y',
xAxisLabel: '',
xAxisShowminmax: false,

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