mirror of
https://github.com/apache/superset.git
synced 2026-06-30 20:05:36 +00:00
Compare commits
1 Commits
esbuild-in
...
boxplot-ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
120a92728e |
2
.github/workflows/embedded-sdk-release.yml
vendored
2
.github/workflows/embedded-sdk-release.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
node-version: "20"
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm run ci:release
|
||||
|
||||
2
.github/workflows/embedded-sdk-test.yml
vendored
2
.github/workflows/embedded-sdk-test.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
node-version: "20"
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
67
.github/workflows/ephemeral-env.yml
vendored
67
.github/workflows/ephemeral-env.yml
vendored
@@ -50,45 +50,17 @@ jobs:
|
||||
echo "result=up" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "result=noop" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get event SHA
|
||||
id: get-sha
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
let prSha;
|
||||
|
||||
// If event is workflow_dispatch, use the issue_number from inputs
|
||||
if (context.eventName === "workflow_dispatch") {
|
||||
const prNumber = "${{ github.event.inputs.issue_number }}";
|
||||
if (!prNumber) {
|
||||
console.log("No PR number found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch PR details using the provided issue_number
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
prSha = pr.head.sha;
|
||||
} else {
|
||||
// If it's not workflow_dispatch, use the PR head sha from the event
|
||||
prSha = context.payload.pull_request.head.sha;
|
||||
}
|
||||
|
||||
console.log(`PR SHA: ${prSha}`);
|
||||
core.setOutput("sha", prSha);
|
||||
run: |
|
||||
echo "sha=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Looking for feature flags in PR description
|
||||
uses: actions/github-script@v7
|
||||
id: eval-feature-flags
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
with:
|
||||
script: |
|
||||
const description = context.payload.pull_request
|
||||
@@ -109,7 +81,6 @@ jobs:
|
||||
|
||||
- name: Reply with confirmation comment
|
||||
uses: actions/github-script@v7
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -190,9 +161,8 @@ jobs:
|
||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||
ECR_REPOSITORY: superset-ci
|
||||
IMAGE_TAG: apache/superset:${{ needs.ephemeral-env-label.outputs.sha }}-ci
|
||||
PR_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
docker tag $IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:pr-$PR_NUMBER-ci
|
||||
docker tag $IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-ci
|
||||
docker push -a $ECR_REGISTRY/$ECR_REPOSITORY
|
||||
|
||||
ephemeral-env-up:
|
||||
@@ -223,13 +193,11 @@ jobs:
|
||||
- name: Check target image exists in ECR
|
||||
id: check-image
|
||||
continue-on-error: true
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
aws ecr describe-images \
|
||||
--registry-id $(echo "${{ steps.login-ecr.outputs.registry }}" | grep -Eo "^[0-9]+") \
|
||||
--repository-name superset-ci \
|
||||
--image-ids imageTag=pr-$PR_NUMBER-ci
|
||||
--image-ids imageTag=pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-ci
|
||||
|
||||
- name: Fail on missing container image
|
||||
if: steps.check-image.outcome == 'failure'
|
||||
@@ -239,7 +207,7 @@ jobs:
|
||||
script: |
|
||||
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.pull_request.number }},
|
||||
issue_number: ${{ github.event.inputs.issue_number || github.event.issue.number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: errMsg
|
||||
@@ -252,7 +220,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.pull_request.number }}-ci
|
||||
image: ${{ steps.login-ecr.outputs.registry }}/superset-ci:pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-ci
|
||||
|
||||
- name: Update env vars in the Amazon ECS task definition
|
||||
run: |
|
||||
@@ -261,30 +229,29 @@ 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.pull_request.number }}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
|
||||
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
|
||||
- name: Create ECS service
|
||||
id: create-service
|
||||
if: steps.describe-services.outputs.active != 'true'
|
||||
env:
|
||||
ECR_SUBNETS: subnet-0e15a5034b4121710,subnet-0e8efef4a72224974
|
||||
ECR_SECURITY_GROUP: sg-092ff3a6ae0574d91
|
||||
PR_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
aws ecs create-service \
|
||||
--cluster superset-ci \
|
||||
--service-name pr-$PR_NUMBER-service \
|
||||
--service-name pr-${{ github.event.inputs.issue_number || 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=$PR_NUMBER key=github_user,value=${{ github.actor }}
|
||||
--tags key=pr,value=${{ github.event.inputs.issue_number || 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.pull_request.number }}-service
|
||||
service: pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-service
|
||||
cluster: superset-ci
|
||||
wait-for-service-stability: true
|
||||
wait-for-minutes: 10
|
||||
@@ -292,7 +259,7 @@ 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.pull_request.number }}-service | jq '.taskArns | first')" >> $GITHUB_OUTPUT
|
||||
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
|
||||
- name: Get network interface
|
||||
id: get-eni
|
||||
run: |
|
||||
@@ -307,22 +274,20 @@ jobs:
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
const issue_number = context.payload.inputs?.issue_number || context.issue.number;
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issue_number,
|
||||
issue_number: ${{ github.event.inputs.issue_number || github.event.issue.number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `@${{ github.actor }} 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.actor }} 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
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
const issue_number = context.payload.inputs?.issue_number || context.issue.number;
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issue_number,
|
||||
issue_number: ${{ github.event.inputs.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.'
|
||||
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -24,7 +24,13 @@ jobs:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
name: Bump version and publish package(s)
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -40,11 +46,11 @@ jobs:
|
||||
git fetch --prune --unshallow
|
||||
git tag -d `git tag | grep -E '^trigger-'`
|
||||
|
||||
- name: Install Node.js
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
if: env.HAS_TAGS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Cache npm
|
||||
if: env.HAS_TAGS
|
||||
|
||||
@@ -26,6 +26,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: ["chrome"]
|
||||
node: [20]
|
||||
env:
|
||||
SUPERSET_ENV: development
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
@@ -65,7 +66,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
node-version: ${{ matrix.node }}
|
||||
- name: Install npm dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
|
||||
@@ -28,6 +28,9 @@ jobs:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [20]
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v4
|
||||
@@ -38,7 +41,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
node-version: ${{ matrix.node }}
|
||||
- name: Install eyes-storybook dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
|
||||
4
.github/workflows/superset-docs-deploy.yml
vendored
4
.github/workflows/superset-docs-deploy.yml
vendored
@@ -35,10 +35,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
- name: Set up Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
node-version: '20'
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
- uses: actions/setup-java@v4
|
||||
|
||||
4
.github/workflows/superset-docs-verify.yml
vendored
4
.github/workflows/superset-docs-verify.yml
vendored
@@ -60,10 +60,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
- name: Set up Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
node-version: '20'
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
|
||||
2
.github/workflows/superset-e2e.yml
vendored
2
.github/workflows/superset-e2e.yml
vendored
@@ -109,7 +109,7 @@ jobs:
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
node-version: "20"
|
||||
- name: Install npm dependencies
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
|
||||
2
.github/workflows/superset-translations.yml
vendored
2
.github/workflows/superset-translations.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
if: steps.check.outputs.frontend
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
node-version: '18'
|
||||
- name: Install dependencies
|
||||
if: steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
|
||||
2
.github/workflows/tech-debt.yml
vendored
2
.github/workflows/tech-debt.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,7 +21,6 @@
|
||||
*.swp
|
||||
__pycache__
|
||||
|
||||
.aider*
|
||||
.local
|
||||
.cache
|
||||
.bento*
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -18,19 +18,16 @@
|
||||
######################################################################
|
||||
# Node stage to deal with static asset construction
|
||||
######################################################################
|
||||
ARG PY_VER=3.11.11-slim-bookworm
|
||||
ARG PY_VER=3.11-slim-bookworm
|
||||
|
||||
# If BUILDPLATFORM is null, set it to 'amd64' (or leave as is otherwise).
|
||||
ARG BUILDPLATFORM=${BUILDPLATFORM:-amd64}
|
||||
|
||||
# Include translations in the final build
|
||||
ARG BUILD_TRANSLATIONS="false"
|
||||
|
||||
######################################################################
|
||||
# superset-node-ci used as a base for building frontend assets and CI
|
||||
######################################################################
|
||||
FROM --platform=${BUILDPLATFORM} node:20-bullseye-slim AS superset-node-ci
|
||||
ARG BUILD_TRANSLATIONS
|
||||
ARG BUILD_TRANSLATIONS="false" # Include translations in the final build
|
||||
ENV BUILD_TRANSLATIONS=${BUILD_TRANSLATIONS}
|
||||
ARG DEV_MODE="false" # Skip frontend build in dev mode
|
||||
ENV DEV_MODE=${DEV_MODE}
|
||||
@@ -125,13 +122,10 @@ ENV PATH="/app/.venv/bin:${PATH}"
|
||||
######################################################################
|
||||
FROM python-base AS python-translation-compiler
|
||||
|
||||
ARG BUILD_TRANSLATIONS
|
||||
ENV BUILD_TRANSLATIONS=${BUILD_TRANSLATIONS}
|
||||
|
||||
# Install Python dependencies using docker/pip-install.sh
|
||||
COPY requirements/translations.txt requirements/
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
. /app/.venv/bin/activate && /app/docker/pip-install.sh --requires-build-essential -r requirements/translations.txt
|
||||
/app/docker/pip-install.sh --requires-build-essential -r requirements/translations.txt
|
||||
|
||||
COPY superset/translations/ /app/translations_mo/
|
||||
RUN if [ "$BUILD_TRANSLATIONS" = "true" ]; then \
|
||||
|
||||
@@ -137,7 +137,6 @@ Here are some of the major database solutions that are supported:
|
||||
<img src="https://superset.apache.org/img/databases/sap-hana.png" alt="oceanbase" border="0" width="220" />
|
||||
<img src="https://superset.apache.org/img/databases/denodo.png" alt="denodo" border="0" width="200" />
|
||||
<img src="https://superset.apache.org/img/databases/ydb.svg" alt="ydb" border="0" width="200" />
|
||||
<img src="https://superset.apache.org/img/databases/tdengine.png" alt="TDengine" border="0" width="200" />
|
||||
</p>
|
||||
|
||||
**A more comprehensive list of supported databases** along with the configuration instructions can be found [here](https://superset.apache.org/docs/configuration/databases).
|
||||
|
||||
@@ -30,12 +30,12 @@ RUN apt-get install -y apt-transport-https apt-utils
|
||||
# Install superset dependencies
|
||||
# https://superset.apache.org/docs/installation/installing-superset-from-scratch
|
||||
RUN apt-get install -y build-essential libssl-dev \
|
||||
libffi-dev python3-dev libsasl2-dev libldap2-dev libxi-dev chromium zstd
|
||||
libffi-dev python3-dev libsasl2-dev libldap2-dev libxi-dev chromium
|
||||
|
||||
# Install nodejs for custom build
|
||||
# https://nodejs.org/en/download/package-manager/
|
||||
RUN set -eux; \
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | bash -; \
|
||||
curl -sL https://deb.nodesource.com/setup_18.x | bash -; \
|
||||
apt-get install -y nodejs; \
|
||||
node --version;
|
||||
RUN if ! which npm; then apt-get install -y npm; fi
|
||||
@@ -64,7 +64,7 @@ RUN pip install --upgrade setuptools pip \
|
||||
RUN flask fab babel-compile --target superset/translations
|
||||
|
||||
ENV PATH=/home/superset/superset/bin:$PATH \
|
||||
PYTHONPATH=/home/superset/superset/ \
|
||||
PYTHONPATH=/home/superset/superset/:$PYTHONPATH \
|
||||
SUPERSET_TESTENV=true
|
||||
COPY from_tarball_entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@@ -29,16 +29,13 @@ RUN apt-get install -y apt-transport-https apt-utils
|
||||
|
||||
# Install superset dependencies
|
||||
# https://superset.apache.org/docs/installation/installing-superset-from-scratch
|
||||
RUN apt-get install -y subversion build-essential libssl-dev \
|
||||
libffi-dev python3-dev libsasl2-dev libldap2-dev libxi-dev chromium zstd
|
||||
RUN apt-get install -y build-essential libssl-dev \
|
||||
libffi-dev python3-dev libsasl2-dev libldap2-dev libxi-dev chromium
|
||||
|
||||
# Install nodejs for custom build
|
||||
# https://nodejs.org/en/download/package-manager/
|
||||
RUN set -eux; \
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | bash -; \
|
||||
apt-get install -y nodejs; \
|
||||
node --version;
|
||||
RUN if ! which npm; then apt-get install -y npm; fi
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
RUN mkdir -p /home/superset
|
||||
RUN chown superset /home/superset
|
||||
@@ -49,12 +46,14 @@ ARG VERSION
|
||||
# Can fetch source from svn or copy tarball from local mounted directory
|
||||
RUN svn co https://dist.apache.org/repos/dist/dev/superset/$VERSION ./
|
||||
RUN tar -xvf *.tar.gz
|
||||
WORKDIR /home/superset/apache-superset-$VERSION/superset-frontend
|
||||
WORKDIR apache-superset-$VERSION
|
||||
|
||||
RUN npm ci \
|
||||
RUN cd superset-frontend \
|
||||
&& npm ci \
|
||||
&& npm run build \
|
||||
&& rm -rf node_modules
|
||||
|
||||
|
||||
WORKDIR /home/superset/apache-superset-$VERSION
|
||||
RUN pip install --upgrade setuptools pip \
|
||||
&& pip install -r requirements/base.txt \
|
||||
@@ -63,6 +62,6 @@ RUN pip install --upgrade setuptools pip \
|
||||
RUN flask fab babel-compile --target superset/translations
|
||||
|
||||
ENV PATH=/home/superset/superset/bin:$PATH \
|
||||
PYTHONPATH=/home/superset/superset/
|
||||
PYTHONPATH=/home/superset/superset/:$PYTHONPATH
|
||||
COPY from_tarball_entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@@ -43,8 +43,8 @@ under the License.
|
||||
| can this form post on ResetPasswordView |:heavy_check_mark:|O|O|O|
|
||||
| can this form get on ResetMyPasswordView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can this form post on ResetMyPasswordView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can this form get on UserInfoEditView |:heavy_check_mark:|O|O|O|
|
||||
| can this form post on UserInfoEditView |:heavy_check_mark:|O|O|O|
|
||||
| can this form get on UserInfoEditView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can this form post on UserInfoEditView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can show on UserDBModelView |:heavy_check_mark:|O|O|O|
|
||||
| can edit on UserDBModelView |:heavy_check_mark:|O|O|O|
|
||||
| can delete on UserDBModelView |:heavy_check_mark:|O|O|O|
|
||||
@@ -65,6 +65,7 @@ under the License.
|
||||
| can get on MenuApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can list on AsyncEventsRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can invalidate on CacheRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can function names on Database |:heavy_check_mark:|O|O|O|
|
||||
| can csv upload on Database |:heavy_check_mark:|O|O|O|
|
||||
| can excel upload on Database |:heavy_check_mark:|O|O|O|
|
||||
| can query form data on Api |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
@@ -75,6 +76,7 @@ under the License.
|
||||
| can get on Datasource |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can my queries on SqlLab |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
||||
| can log on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can schemas access for csv upload on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can import dashboards on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can schemas on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can sqllab history on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
||||
@@ -116,6 +118,8 @@ under the License.
|
||||
| menu access on Data |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| menu access on Databases |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| menu access on Datasets |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| menu access on Upload a CSV |:heavy_check_mark:|:heavy_check_mark:|O|O|
|
||||
| menu access on Upload Excel |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| menu access on Charts |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| menu access on Dashboards |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| menu access on SQL Lab |:heavy_check_mark:|O|O|:heavy_check_mark:|
|
||||
@@ -125,6 +129,13 @@ under the License.
|
||||
| all datasource access on all_datasource_access |:heavy_check_mark:|:heavy_check_mark:|O|O|
|
||||
| all database access on all_database_access |:heavy_check_mark:|:heavy_check_mark:|O|O|
|
||||
| all query access on all_query_access |:heavy_check_mark:|O|O|O|
|
||||
| can edit on UserOAuthModelView |:heavy_check_mark:|O|O|O|
|
||||
| can list on UserOAuthModelView |:heavy_check_mark:|O|O|O|
|
||||
| can show on UserOAuthModelView |:heavy_check_mark:|O|O|O|
|
||||
| can userinfo on UserOAuthModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can add on UserOAuthModelView |:heavy_check_mark:|O|O|O|
|
||||
| can delete on UserOAuthModelView |:heavy_check_mark:|O|O|O|
|
||||
| userinfoedit on UserOAuthModelView |:heavy_check_mark:|O|O|O|
|
||||
| can write on DynamicPlugin |:heavy_check_mark:|O|O|O|
|
||||
| can edit on DynamicPlugin |:heavy_check_mark:|O|O|O|
|
||||
| can list on DynamicPlugin |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
@@ -181,6 +192,7 @@ under the License.
|
||||
| can share chart on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can this form get on ColumnarToDatabaseView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can this form post on ColumnarToDatabaseView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| menu access on Upload a Columnar file |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can export on Chart |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can write on DashboardFilterStateRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can read on DashboardFilterStateRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
|
||||
@@ -24,9 +24,8 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
- [31976](https://github.com/apache/superset/pull/31976) Removed the `DISABLE_LEGACY_DATASOURCE_EDITOR` feature flag. The previous value of the feature flag was `True` and now the feature is permanently removed.
|
||||
- [31959](https://github.com/apache/superset/pull/32000) Removes CSV_UPLOAD_MAX_SIZE config, use your web server to control file upload size.
|
||||
- [31959](https://github.com/apache/superset/pull/31959) Removes the following endpoints from data uploads: `/api/v1/database/<id>/<file type>_upload` and `/api/v1/database/<file type>_metadata`, in favour of new one (Details on the PR). And simplifies permissions.
|
||||
- [31959](https://github.com/apache/superset/pull/31959) Removes the following endpoints from data uploads: /api/v1/database/<id>/<file type>_upload and /api/v1/database/<file type>_metadata, in favour of new one (Details on the PR). And simplifies permissions.
|
||||
- [31844](https://github.com/apache/superset/pull/31844) The `ALERT_REPORTS_EXECUTE_AS` and `THUMBNAILS_EXECUTE_AS` config parameters have been renamed to `ALERT_REPORTS_EXECUTORS` and `THUMBNAILS_EXECUTORS` respectively. A new config flag `CACHE_WARMUP_EXECUTORS` has also been introduced to be able to control which user is used to execute cache warmup tasks. Finally, the config flag `THUMBNAILS_SELENIUM_USER` has been removed. To use a fixed executor for async tasks, use the new `FixedExecutor` class. See the config and docs for more info on setting up different executor profiles.
|
||||
- [31894](https://github.com/apache/superset/pull/31894) Domain sharding is deprecated in favor of HTTP2. The `SUPERSET_WEBSERVER_DOMAINS` configuration will be removed in the next major version (6.0)
|
||||
- [31794](https://github.com/apache/superset/pull/31794) Removed the previously deprecated `DASHBOARD_CROSS_FILTERS` feature flag
|
||||
@@ -46,7 +45,7 @@ assists people when migrating to a new version.
|
||||
- [25166](https://github.com/apache/superset/pull/25166) Changed the default configuration of `UPLOAD_FOLDER` from `/app/static/uploads/` to `/static/uploads/`. It also removed the unused `IMG_UPLOAD_FOLDER` and `IMG_UPLOAD_URL` configuration options.
|
||||
- [30284](https://github.com/apache/superset/pull/30284) Deprecated GLOBAL_ASYNC_QUERIES_REDIS_CONFIG in favor of the new GLOBAL_ASYNC_QUERIES_CACHE_BACKEND configuration. To leverage Redis Sentinel, set CACHE_TYPE to RedisSentinelCache, or use RedisCache for standalone Redis
|
||||
- [31961](https://github.com/apache/superset/pull/31961) Upgraded React from version 16.13.1 to 17.0.2. If you are using custom frontend extensions or plugins, you may need to update them to be compatible with React 17.
|
||||
- [31260](https://github.com/apache/superset/pull/31260) Docker images now use `uv pip install` instead of `pip install` to manage the python envrionment. Most docker-based deployments will be affected, whether you derive one of the published images, or have custom bootstrap script that install python libraries (drivers)
|
||||
|
||||
|
||||
### Potential Downtime
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ are compatible with Superset.
|
||||
| [MySQL](/docs/configuration/databases#mysql) | `pip install mysqlclient` | `mysql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||
| [OceanBase](/docs/configuration/databases#oceanbase) | `pip install oceanbase_py` | `oceanbase://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||
| [Oracle](/docs/configuration/databases#oracle) | `pip install cx_Oracle` | `oracle://` |
|
||||
| [Parseable](/docs/configuration/databases#parseable) | `pip install sqlalchemy-parseable` | `parseable://<UserName>:<DBPassword>@<Database Host>/<Stream Name>` |
|
||||
| [PostgreSQL](/docs/configuration/databases#postgres) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||
| [Presto](/docs/configuration/databases#presto) | `pip install pyhive` | `presto://` |
|
||||
| [Rockset](/docs/configuration/databases#rockset) | `pip install rockset-sqlalchemy` | `rockset://<api_key>:@<api_server>` |
|
||||
@@ -78,7 +77,6 @@ are compatible with Superset.
|
||||
| [Snowflake](/docs/configuration/databases#snowflake) | `pip install snowflake-sqlalchemy` | `snowflake://{user}:{password}@{account}.{region}/{database}?role={role}&warehouse={warehouse}` |
|
||||
| SQLite | No additional library needed | `sqlite://path/to/file.db?check_same_thread=false` |
|
||||
| [SQL Server](/docs/configuration/databases#sql-server) | `pip install pymssql` | `mssql+pymssql://` |
|
||||
| [TDengine](/docs/configuration/databases#tdengine) | `pip install taospy` `pip install taos-ws-py` | `taosws://<user>:<password>@<host>:<port>` |
|
||||
| [Teradata](/docs/configuration/databases#teradata) | `pip install teradatasqlalchemy` | `teradatasql://{user}:{password}@{host}` |
|
||||
| [TimescaleDB](/docs/configuration/databases#timescaledb) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>:<Port>/<Database Name>` |
|
||||
| [Trino](/docs/configuration/databases#trino) | `pip install trino` | `trino://{username}:{password}@{hostname}:{port}/{catalog}` |
|
||||
@@ -1076,23 +1074,6 @@ The connection string is formatted as follows:
|
||||
oracle://<username>:<password>@<hostname>:<port>
|
||||
```
|
||||
|
||||
#### Parseable
|
||||
|
||||
[Parseable](https://www.parseable.io) is a distributed log analytics database that provides SQL-like query interface for log data. The recommended connector library is [sqlalchemy-parseable](https://github.com/parseablehq/sqlalchemy-parseable).
|
||||
|
||||
The connection string is formatted as follows:
|
||||
|
||||
```
|
||||
parseable://<username>:<password>@<hostname>:<port>/<stream_name>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
parseable://admin:admin@demo.parseable.com:443/ingress-nginx
|
||||
```
|
||||
|
||||
Note: The stream_name in the URI represents the Parseable logstream you want to query. You can use both HTTP (port 80) and HTTPS (port 443) connections.
|
||||
|
||||
|
||||
#### Apache Pinot
|
||||
@@ -1355,24 +1336,6 @@ starrocks://<User>:<Password>@<Host>:<Port>/<Catalog>.<Database>
|
||||
StarRocks maintains their Superset docuementation [here](https://docs.starrocks.io/docs/integrations/BI_integrations/Superset/).
|
||||
:::
|
||||
|
||||
#### TDengine
|
||||
|
||||
[TDengine](https://www.tdengine.com) is a High-Performance, Scalable Time-Series Database for Industrial IoT and provides SQL-like query interface.
|
||||
|
||||
The recommended connector library for TDengine is [taospy](https://pypi.org/project/taospy/) and [taos-ws-py](https://pypi.org/project/taos-ws-py/)
|
||||
|
||||
The expected connection string is formatted as follows:
|
||||
|
||||
```
|
||||
taosws://<user>:<password>@<host>:<port>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
taosws://root:taosdata@127.0.0.1:6041
|
||||
```
|
||||
|
||||
#### Teradata
|
||||
|
||||
The recommended connector library is
|
||||
|
||||
@@ -45,7 +45,7 @@ This is the core application. Superset operates like this:
|
||||
|
||||
This is where chart and dashboard definitions, user information, logs, etc. are stored. Superset is tested to work with PostgreSQL and MySQL databases as the metadata database (not be confused with a data source like your data warehouse, which could be a much greater variety of options like Snowflake, Redshift, etc.).
|
||||
|
||||
Some installation methods like our Quickstart and PyPI come configured by default to use a SQLite on-disk database. And in a Docker Compose installation, the data would be stored in a PostgreSQL container volume. Neither of these cases are recommended for production instances of Superset.
|
||||
Some installation methods like our Quickstart and PyPI come configured by default to use a SQLite on-disk database. And in a Docker Compose installation, the data would be stored in a PostgresQL container volume. Neither of these cases are recommended for production instances of Superset.
|
||||
|
||||
For production, a properly-configured, managed, standalone database is recommended. No matter what database you use, you should plan to back it up regularly.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Since `docker compose` is primarily designed to run a set of containers on **a s
|
||||
and can't support requirements for **high availability**, we do not support nor recommend
|
||||
using our `docker compose` constructs to support production-type use-cases. For single host
|
||||
environments, we recommend using [minikube](https://minikube.sigs.k8s.io/docs/start/) along
|
||||
with our [installing on k8s](https://superset.apache.org/docs/installation/running-on-kubernetes)
|
||||
our [installing on k8s](https://superset.apache.org/docs/installation/running-on-kubernetes)
|
||||
documentation.
|
||||
:::
|
||||
|
||||
@@ -43,6 +43,7 @@ Note that there are 3 major ways we support to run `docker compose`:
|
||||
`export TAG=4.0.0-dev` or `export TAG=3.0.0-dev`, with `latest-dev` being the default.
|
||||
That's because The `dev` builds happen to package the `psycopg2-binary` required to connect
|
||||
to the Postgres database launched as part of the `docker compose` builds.
|
||||
``
|
||||
|
||||
More on these two approaches after setting up the requirements for either.
|
||||
|
||||
|
||||
@@ -150,9 +150,6 @@ Superset requires a Python DB-API database driver and a SQLAlchemy
|
||||
dialect to be installed for each datastore you want to connect to.
|
||||
|
||||
See [Install Database Drivers](/docs/configuration/databases) for more information.
|
||||
It is recommended that you refer to versions listed in
|
||||
[pyproject.toml](https://github.com/apache/superset/blob/master/pyproject.toml)
|
||||
instead of hard-coding them in your bootstrap script, as seen below.
|
||||
|
||||
:::
|
||||
|
||||
@@ -160,9 +157,9 @@ The following example installs the drivers for BigQuery and Elasticsearch, allow
|
||||
```yaml
|
||||
bootstrapScript: |
|
||||
#!/bin/bash
|
||||
uv pip install .[postgres] \
|
||||
.[bigquery] \
|
||||
.[elasticsearch] &&\
|
||||
pip install psycopg2==2.9.6 \
|
||||
sqlalchemy-bigquery==1.6.1 \
|
||||
elasticsearch-dbapi==0.2.5 &&\
|
||||
if [ ! -f ~/bootstrap ]; then echo "Running Superset with uid {{ .Values.runAsUser }}" > ~/bootstrap; fi
|
||||
```
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"less": "^4.2.2",
|
||||
"less": "^4.2.1",
|
||||
"less-loader": "^11.0.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -137,9 +137,4 @@ export const Databases = [
|
||||
href: 'https://www.denodo.com/',
|
||||
imgName: 'denodo.png',
|
||||
},
|
||||
{
|
||||
title: 'TDengine',
|
||||
href: 'https://www.tdengine.com/',
|
||||
imgName: 'tdengine.png',
|
||||
},
|
||||
];
|
||||
|
||||
BIN
docs/static/img/databases/tdengine.png
vendored
BIN
docs/static/img/databases/tdengine.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
@@ -6359,10 +6359,10 @@ less-loader@^11.0.0:
|
||||
resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-11.1.4.tgz#e8a070844efaefbe59b978acaf57b9d3e868cf08"
|
||||
integrity sha512-6/GrYaB6QcW6Vj+/9ZPgKKs6G10YZai/l/eJ4SLwbzqNTBsAqt5hSLVF47TgsiBxV1P6eAU0GYRH3YRuQU9V3A==
|
||||
|
||||
less@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.2.2.tgz#4b59ede113933b58ab152190edf9180fc36846d8"
|
||||
integrity sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==
|
||||
less@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.2.1.tgz#fe4c9848525ab44614c0cf2c00abd8d031bb619a"
|
||||
integrity sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==
|
||||
dependencies:
|
||||
copy-anything "^2.0.1"
|
||||
parse-node-version "^1.0.1"
|
||||
|
||||
@@ -87,6 +87,7 @@ dependencies = [
|
||||
"redis>=4.6.0, <5.0",
|
||||
"selenium>=4.14.0, <5.0",
|
||||
"shillelagh[gsheetsapi]>=1.2.18, <2.0",
|
||||
"shortid",
|
||||
"sshtunnel>=0.4.0, <0.5",
|
||||
"simplejson>=3.15.0",
|
||||
"slack_sdk>=3.19.0, <4",
|
||||
@@ -155,7 +156,6 @@ ocient = [
|
||||
"geojson",
|
||||
]
|
||||
oracle = ["cx-Oracle>8.0.0, <8.1"]
|
||||
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
|
||||
pinot = ["pinotdb>=5.0.0, <6.0.0"]
|
||||
playwright = ["playwright>=1.37.0, <2"]
|
||||
postgres = ["psycopg2-binary==2.9.6"]
|
||||
@@ -172,10 +172,6 @@ spark = [
|
||||
"tableschema",
|
||||
"thrift>=0.14.1, <1",
|
||||
]
|
||||
tdengine = [
|
||||
"taospy>=2.7.21",
|
||||
"taos-ws-py>=0.3.8"
|
||||
]
|
||||
teradata = ["teradatasql>=16.20.0.23"]
|
||||
thumbnails = ["Pillow>=10.0.1, <11"]
|
||||
vertica = ["sqlalchemy-vertica-python>=0.5.9, < 0.6"]
|
||||
|
||||
@@ -329,6 +329,8 @@ selenium==4.27.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
shillelagh==1.2.18
|
||||
# via apache-superset (pyproject.toml)
|
||||
shortid==0.1.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
simplejson==3.19.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
six==1.16.0
|
||||
|
||||
@@ -738,6 +738,10 @@ shillelagh==1.2.18
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
shortid==0.1.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
simplejson==3.19.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
v20.16.0
|
||||
@@ -74,7 +74,7 @@ module.exports = {
|
||||
'file-progress',
|
||||
'lodash',
|
||||
'theme-colors',
|
||||
'i18n-strings',
|
||||
'translation-vars',
|
||||
'react-prefer-function-component',
|
||||
'prettier',
|
||||
],
|
||||
@@ -284,7 +284,7 @@ module.exports = {
|
||||
],
|
||||
rules: {
|
||||
'theme-colors/no-literal-colors': 0,
|
||||
'i18n-strings/no-template-vars': 0,
|
||||
'translation-vars/no-template-vars': 0,
|
||||
'no-restricted-imports': 0,
|
||||
'react/no-void-elements': 0,
|
||||
},
|
||||
@@ -292,7 +292,7 @@ module.exports = {
|
||||
],
|
||||
rules: {
|
||||
'theme-colors/no-literal-colors': 'error',
|
||||
'i18n-strings/no-template-vars': ['error', true],
|
||||
'translation-vars/no-template-vars': ['error', true],
|
||||
camelcase: [
|
||||
'error',
|
||||
{
|
||||
@@ -354,14 +354,6 @@ module.exports = {
|
||||
name: 'lodash/memoize',
|
||||
message: 'Lodash Memoize is unsafe! Please use memoize-one instead',
|
||||
},
|
||||
{
|
||||
name: '@testing-library/react',
|
||||
message: 'Please use spec/helpers/testing-library instead',
|
||||
},
|
||||
{
|
||||
name: '@testing-library/react-dom-utils',
|
||||
message: 'Please use spec/helpers/testing-library instead',
|
||||
},
|
||||
],
|
||||
patterns: ['antd/*'],
|
||||
},
|
||||
|
||||
@@ -47,12 +47,12 @@ describe.skip('Dashboard top-level controls', () => {
|
||||
// Solution: pause the network before clicking, assert, then unpause network.
|
||||
cy.get('[data-test="refresh-chart-menu-item"]').should(
|
||||
'have.class',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
);
|
||||
waitForChartLoad(mapSpec);
|
||||
cy.get('[data-test="refresh-chart-menu-item"]').should(
|
||||
'not.have.class',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -65,7 +65,7 @@ describe.skip('Dashboard top-level controls', () => {
|
||||
cy.get('[aria-label="more-horiz"]').click();
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
|
||||
'not.have.class',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
);
|
||||
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').click({
|
||||
@@ -73,7 +73,7 @@ describe.skip('Dashboard top-level controls', () => {
|
||||
});
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
|
||||
'have.class',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
);
|
||||
|
||||
// wait all charts force refreshed.
|
||||
@@ -94,7 +94,7 @@ describe.skip('Dashboard top-level controls', () => {
|
||||
cy.get('[aria-label="more-horiz"]').click();
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').and(
|
||||
'not.have.class',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,14 +54,15 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
|
||||
interceptV1ChartData();
|
||||
}
|
||||
|
||||
cy.get('.antd5-dropdown:not(.antd5-dropdown-hidden)')
|
||||
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
|
||||
.first()
|
||||
.should('be.visible')
|
||||
.find("[role='menu'] [role='menuitem']")
|
||||
.contains(/^Drill by$/)
|
||||
.trigger('mouseover', { force: true });
|
||||
|
||||
cy.get(
|
||||
'.antd5-dropdown-menu-submenu:not(.antd5-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
|
||||
@@ -61,14 +61,15 @@ function drillToDetail(targetMenuItem: string) {
|
||||
const drillToDetailBy = (targetDrill: string) => {
|
||||
interceptSamples();
|
||||
|
||||
cy.get('.antd5-dropdown:not(.antd5-dropdown-hidden)')
|
||||
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
|
||||
.first()
|
||||
.should('be.visible')
|
||||
.find("[role='menu'] [role='menuitem']")
|
||||
.contains(/^Drill to detail by$/)
|
||||
.trigger('mouseover', { force: true });
|
||||
|
||||
cy.get(
|
||||
'.antd5-dropdown-menu-submenu:not(.antd5-dropdown-menu-submenu-hidden) [data-test="drill-to-detail-by-submenu"]',
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-to-detail-by-submenu"]',
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
|
||||
@@ -57,16 +57,16 @@ function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') {
|
||||
.trigger('mouseover');
|
||||
|
||||
if (orientation === 'vertical') {
|
||||
cy.get('.antd5-dropdown-menu-item-selected')
|
||||
cy.get('.antd5-menu-item-selected')
|
||||
.contains('Horizontal (Top)')
|
||||
.should('exist');
|
||||
cy.get('.antd5-dropdown-menu-item').contains('Vertical (Left)').click();
|
||||
cy.get('.antd5-menu-item').contains('Vertical (Left)').click();
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
} else {
|
||||
cy.get('.antd5-dropdown-menu-item-selected')
|
||||
cy.get('.antd5-menu-item-selected')
|
||||
.contains('Vertical (Left)')
|
||||
.should('exist');
|
||||
cy.get('.antd5-dropdown-menu-item').contains('Horizontal (Top)').click();
|
||||
cy.get('.antd5-menu-item').contains('Horizontal (Top)').click();
|
||||
cy.getBySel('loading-indicator').should('exist');
|
||||
cy.getBySel('filter-bar').should('exist');
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
@@ -161,7 +161,7 @@ describe('Horizontal FilterBar', () => {
|
||||
cy.getBySel('filter-control-name')
|
||||
.contains('test_12')
|
||||
.should('not.be.visible');
|
||||
cy.get('.antd5-popover-inner').scrollTo('bottom');
|
||||
cy.get('.ant-popover-inner-content').scrollTo('bottom');
|
||||
cy.getBySel('filter-control-name').contains('test_12').should('be.visible');
|
||||
});
|
||||
|
||||
@@ -226,7 +226,7 @@ describe('Horizontal FilterBar', () => {
|
||||
cy.getBySel('slice-header').within(() => {
|
||||
cy.get('.filter-counts').trigger('mouseover');
|
||||
});
|
||||
cy.getBySel('filter-status-popover').contains('test_9').click();
|
||||
cy.get('.filterStatusPopover').contains('test_9').click();
|
||||
cy.getBySel('dropdown-content').should('be.visible');
|
||||
cy.get('.ant-select-focused').should('be.visible');
|
||||
});
|
||||
|
||||
@@ -456,19 +456,19 @@ export function applyAdvancedTimeRangeFilterOnDashboard(
|
||||
endRange?: string,
|
||||
) {
|
||||
cy.get('.control-label').contains('RANGE TYPE').should('be.visible');
|
||||
cy.get('.antd5-popover-content .ant-select-selector')
|
||||
cy.get('.ant-popover-content .ant-select-selector')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.get(`[label="Advanced"]`).should('be.visible').click();
|
||||
cy.get('.section-title').contains('Advanced Time Range').should('be.visible');
|
||||
if (startRange) {
|
||||
cy.get('.antd5-popover-inner-content')
|
||||
cy.get('.ant-popover-inner-content')
|
||||
.find('[class^=ant-input]')
|
||||
.first()
|
||||
.type(`${startRange}`);
|
||||
}
|
||||
if (endRange) {
|
||||
cy.get('.antd5-popover-inner-content')
|
||||
cy.get('.ant-popover-inner-content')
|
||||
.find('[class^=ant-input]')
|
||||
.last()
|
||||
.type(`${endRange}`);
|
||||
|
||||
@@ -31,35 +31,35 @@ const SAMPLE_DASHBOARDS_INDEXES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
function openDashboardsAddedTo() {
|
||||
cy.getBySel('actions-trigger').click();
|
||||
cy.get('.antd5-dropdown-menu-submenu-title')
|
||||
cy.get('.ant-dropdown-menu-submenu-title')
|
||||
.contains('On dashboards')
|
||||
.trigger('mouseover', { force: true });
|
||||
}
|
||||
|
||||
function closeDashboardsAddedTo() {
|
||||
cy.get('.antd5-dropdown-menu-submenu-title')
|
||||
cy.get('.ant-dropdown-menu-submenu-title')
|
||||
.contains('On dashboards')
|
||||
.trigger('mouseout', { force: true });
|
||||
cy.getBySel('actions-trigger').click();
|
||||
}
|
||||
|
||||
function verifyDashboardsSubmenuItem(dashboardName) {
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').contains(dashboardName);
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').contains(dashboardName);
|
||||
closeDashboardsAddedTo();
|
||||
}
|
||||
|
||||
function verifyDashboardSearch() {
|
||||
openDashboardsAddedTo();
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup')
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup')
|
||||
.find('input[placeholder="Search"]')
|
||||
.type('1');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').contains('1 - Sample dashboard');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup')
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').contains('1 - Sample dashboard');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup')
|
||||
.find('input[placeholder="Search"]')
|
||||
.type('Blahblah');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').contains('No results found');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup')
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').contains('No results found');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup')
|
||||
.find('[aria-label="close-circle"]')
|
||||
.click();
|
||||
closeDashboardsAddedTo();
|
||||
@@ -68,8 +68,8 @@ function verifyDashboardSearch() {
|
||||
function verifyDashboardLink() {
|
||||
interceptDashboardGet();
|
||||
openDashboardsAddedTo();
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup a')
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup a')
|
||||
.first()
|
||||
.invoke('removeAttr', 'target')
|
||||
.click();
|
||||
|
||||
@@ -51,8 +51,8 @@ describe('Datasource control', () => {
|
||||
)
|
||||
.first()
|
||||
.focus();
|
||||
cy.focused().clear({ force: true });
|
||||
cy.focused().type(`${newMetricName}{enter}`, { force: true });
|
||||
cy.focused().clear();
|
||||
cy.focused().type(`${newMetricName}{enter}`);
|
||||
|
||||
cy.get('[data-test="datasource-modal-save"]').click();
|
||||
cy.get('.antd5-modal-confirm-btns button').contains('OK').click();
|
||||
|
||||
@@ -36,10 +36,10 @@ describe('Download Chart > Bar chart', () => {
|
||||
};
|
||||
|
||||
cy.visitChartByParams(formData);
|
||||
cy.get('.header-with-actions .antd5-dropdown-trigger').click();
|
||||
cy.get(':nth-child(3) > .antd5-dropdown-menu-submenu-title').click();
|
||||
cy.get('.header-with-actions .ant-dropdown-trigger').click();
|
||||
cy.get(':nth-child(3) > .ant-dropdown-menu-submenu-title').click();
|
||||
cy.get(
|
||||
'.antd5-dropdown-menu-submenu > .antd5-dropdown-menu li:nth-child(3)',
|
||||
'.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)',
|
||||
).click();
|
||||
cy.verifyDownload('.jpg', {
|
||||
contains: true,
|
||||
|
||||
@@ -80,9 +80,9 @@ describe('SqlLab query tabs', () => {
|
||||
// configure some editor settings
|
||||
cy.get(editorInput).type('some random query string', { force: true });
|
||||
cy.get(queryLimitSelector).parent().click({ force: true });
|
||||
cy.get('.antd5-dropdown-menu')
|
||||
cy.get('.ant-dropdown-menu')
|
||||
.last()
|
||||
.find('.antd5-dropdown-menu-item')
|
||||
.find('.ant-dropdown-menu-item')
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
||||
|
||||
@@ -158,10 +158,10 @@ export const sqlLabView = {
|
||||
runButton: '.css-d3dxop',
|
||||
},
|
||||
rowsLimit: {
|
||||
dropdown: '.antd5-dropdown-menu',
|
||||
limitButton: '.antd5-dropdown-menu-item',
|
||||
dropdown: '.ant-dropdown-menu',
|
||||
limitButton: '.ant-dropdown-menu-item',
|
||||
limitButtonText: '.css-151uxnz',
|
||||
limitTextWithValue: '[class="antd5-dropdown-trigger"]',
|
||||
limitTextWithValue: '[class="ant-dropdown-trigger"]',
|
||||
},
|
||||
renderedTableHeader: '.ReactVirtualized__Table__headerRow',
|
||||
renderedTableRow: '.ReactVirtualized__Table__row',
|
||||
@@ -555,7 +555,7 @@ export const exploreView = {
|
||||
timeSection: {
|
||||
timeRangeFilter: dataTestLocator('time-range-trigger'),
|
||||
timeRangeFilterModal: {
|
||||
container: '.antd5-popover-content',
|
||||
container: '.ant-popover-content',
|
||||
footer: '.footer',
|
||||
cancelButton: dataTestLocator('cancel-button'),
|
||||
configureLastTimeRange: {
|
||||
@@ -633,7 +633,7 @@ export const dashboardView = {
|
||||
refreshChart: dataTestLocator('refresh-chart-menu-item'),
|
||||
},
|
||||
threeDotsMenuIcon:
|
||||
'.header-with-actions .right-button-panel .antd5-dropdown-trigger',
|
||||
'.header-with-actions .right-button-panel .ant-dropdown-trigger',
|
||||
threeDotsMenuDropdown: dataTestLocator('header-actions-menu'),
|
||||
refreshDashboard: dataTestLocator('refresh-dashboard-menu-item'),
|
||||
saveAsMenuOption: dataTestLocator('save-as-menu-item'),
|
||||
|
||||
@@ -75,5 +75,4 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
],
|
||||
testTimeout: 10000,
|
||||
};
|
||||
|
||||
3302
superset-frontend/package-lock.json
generated
3302
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -68,8 +68,8 @@
|
||||
"prod": "npm run build",
|
||||
"prune": "rm -rf ./{packages,plugins}/*/{node_modules,lib,esm,tsconfig.tsbuildinfo,package-lock.json} ./.temp_cache",
|
||||
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006",
|
||||
"tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --watch",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent",
|
||||
"tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --watch",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --max-workers=50%",
|
||||
"type": "tsc --noEmit",
|
||||
"update-maps": "jupyter nbconvert --to notebook --execute --inplace 'plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb' -Xfrozen_modules=off",
|
||||
"validate-release": "../RELEASING/validate_this_release.sh"
|
||||
@@ -139,7 +139,6 @@
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
@@ -254,7 +253,7 @@
|
||||
"@storybook/react-webpack5": "8.1.11",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
@@ -302,7 +301,6 @@
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-matchers": "^7.1.2",
|
||||
"esbuild": "^0.20.0",
|
||||
"esbuild-loader": "^4.2.2",
|
||||
"eslint": "^8.56.0",
|
||||
@@ -323,7 +321,8 @@
|
||||
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"eslint-plugin-testing-library": "^6.4.0",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"eslint-plugin-theme-colors": "file:tools/eslint-plugin-theme-colors",
|
||||
"eslint-plugin-translation-vars": "file:tools/eslint-plugin-translation-vars",
|
||||
"exports-loader": "^5.0.0",
|
||||
"fetch-mock": "^7.7.3",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
@@ -332,7 +331,9 @@
|
||||
"ignore-styles": "^5.0.1",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-enzyme": "^7.1.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-enzyme": "^7.1.2",
|
||||
"jest-html-reporter": "^3.10.2",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"jsdom": "^26.0.0",
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Popover } from 'antd-v5';
|
||||
import { Popover } from 'antd';
|
||||
import type ReactAce from 'react-ace';
|
||||
import type { PopoverProps } from 'antd-v5/lib/popover';
|
||||
import type { PopoverProps } from 'antd/lib/popover';
|
||||
import { CalculatorOutlined } from '@ant-design/icons';
|
||||
import { css, styled, useTheme, t } from '@superset-ui/core';
|
||||
|
||||
@@ -72,7 +72,7 @@ export const SQLPopover = (props: PopoverProps & { sqlExpression: string }) => {
|
||||
/>
|
||||
}
|
||||
placement="bottomLeft"
|
||||
arrow={{ pointAtCenter: true }}
|
||||
arrowPointAtCenter
|
||||
title={t('SQL expression')}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -84,12 +84,6 @@ export interface Dataset {
|
||||
filter_select?: boolean;
|
||||
filter_select_enabled?: boolean;
|
||||
column_names?: string[];
|
||||
catalog?: string;
|
||||
schema?: string;
|
||||
table_name?: string;
|
||||
database?: Record<string, unknown>;
|
||||
normalize_columns?: boolean;
|
||||
always_filter_main_dttm?: boolean;
|
||||
}
|
||||
|
||||
export interface ControlPanelState {
|
||||
@@ -521,13 +515,6 @@ export enum SortSeriesType {
|
||||
Avg = 'avg',
|
||||
}
|
||||
|
||||
export type LegendPaddingType = {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
};
|
||||
|
||||
export type SortSeriesData = {
|
||||
sort_series_type: SortSeriesType;
|
||||
sort_series_ascending: boolean;
|
||||
|
||||
@@ -19,23 +19,17 @@
|
||||
|
||||
import { QueryFormMetric, isSavedMetric, isAdhocMetricSimple } from './types';
|
||||
|
||||
export default function getMetricLabel(
|
||||
metric: QueryFormMetric,
|
||||
index?: number,
|
||||
queryFormMetrics?: QueryFormMetric[],
|
||||
verboseMap?: Record<string, string>,
|
||||
): string {
|
||||
let label = '';
|
||||
export default function getMetricLabel(metric: QueryFormMetric): string {
|
||||
if (isSavedMetric(metric)) {
|
||||
label = metric;
|
||||
} else if (metric.label) {
|
||||
({ label } = metric);
|
||||
} else if (isAdhocMetricSimple(metric)) {
|
||||
label = `${metric.aggregate}(${
|
||||
return metric;
|
||||
}
|
||||
if (metric.label) {
|
||||
return metric.label;
|
||||
}
|
||||
if (isAdhocMetricSimple(metric)) {
|
||||
return `${metric.aggregate}(${
|
||||
metric.column.columnName || metric.column.column_name
|
||||
})`;
|
||||
} else {
|
||||
label = metric.sqlExpression;
|
||||
}
|
||||
return verboseMap?.[label] || label;
|
||||
return metric.sqlExpression;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Currency, Maybe, QueryFormMetric } from '../../types';
|
||||
import { Maybe, QueryFormMetric } from '../../types';
|
||||
import { Column } from './Column';
|
||||
|
||||
export type Aggregate =
|
||||
@@ -65,7 +65,7 @@ export interface Metric {
|
||||
certification_details?: Maybe<string>;
|
||||
certified_by?: Maybe<string>;
|
||||
d3format?: Maybe<string>;
|
||||
currency?: Maybe<Currency>;
|
||||
currency?: Maybe<string>;
|
||||
description?: Maybe<string>;
|
||||
is_certified?: boolean;
|
||||
verbose_name?: string;
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { triggerResizeObserver } from 'resize-observer-polyfill';
|
||||
import { promiseTimeout, WithLegend } from '@superset-ui/core';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
let renderChart = jest.fn();
|
||||
let renderLegend = jest.fn();
|
||||
@@ -32,18 +32,18 @@ describe.skip('WithLegend', () => {
|
||||
});
|
||||
|
||||
it('sets className', () => {
|
||||
const { container } = render(
|
||||
const wrapper = shallow(
|
||||
<WithLegend
|
||||
className="test-class"
|
||||
renderChart={renderChart}
|
||||
renderLegend={renderLegend}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelectorAll('.test-class')).toHaveLength(1);
|
||||
expect(wrapper.hasClass('test-class')).toEqual(true);
|
||||
});
|
||||
|
||||
it('renders when renderLegend is not set', () => {
|
||||
const { container } = render(
|
||||
const wrapper = mount(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
width={500}
|
||||
@@ -56,13 +56,13 @@ describe.skip('WithLegend', () => {
|
||||
// Have to delay more than debounceTime (1ms)
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(0);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(0);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const { container } = render(
|
||||
const wrapper = mount(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
width={500}
|
||||
@@ -77,13 +77,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders without width or height', () => {
|
||||
const { container } = render(
|
||||
const wrapper = mount(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
renderChart={renderChart}
|
||||
@@ -96,13 +96,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders legend on the left', () => {
|
||||
const { container } = render(
|
||||
const wrapper = mount(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
position="left"
|
||||
@@ -116,13 +116,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders legend on the right', () => {
|
||||
const { container } = render(
|
||||
const wrapper = mount(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
position="right"
|
||||
@@ -136,13 +136,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders legend on the top', () => {
|
||||
const { container } = render(
|
||||
const wrapper = mount(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
position="top"
|
||||
@@ -156,13 +156,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders legend on the bottom', () => {
|
||||
const { container } = render(
|
||||
const wrapper = mount(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
position="bottom"
|
||||
@@ -176,13 +176,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders legend with justifyContent set', () => {
|
||||
const { container } = render(
|
||||
const wrapper = mount(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
position="right"
|
||||
@@ -197,8 +197,8 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,15 +16,16 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import ChartClient from '../../../src/chart/clients/ChartClient';
|
||||
import ChartDataProvider, {
|
||||
ChartDataProviderProps,
|
||||
} from '../../../src/chart/components/ChartDataProvider';
|
||||
import { bigNumberFormData } from '../fixtures/formData';
|
||||
|
||||
// Keep existing mock setup
|
||||
// Note: the mock implementation of these function directly affects the expected results below
|
||||
const defaultMockLoadFormData = jest.fn(({ formData }: { formData: unknown }) =>
|
||||
Promise.resolve(formData),
|
||||
);
|
||||
@@ -49,6 +50,7 @@ const mockLoadQueryData = jest.fn<Promise<unknown>, unknown[]>(
|
||||
);
|
||||
|
||||
const actual = jest.requireActual('../../../src/chart/clients/ChartClient');
|
||||
// ChartClient is now a mock
|
||||
jest.spyOn(actual, 'default').mockImplementation(() => ({
|
||||
loadDatasource: mockLoadDatasource,
|
||||
loadFormData: mockLoadFormData,
|
||||
@@ -60,6 +62,7 @@ const ChartClientMock = ChartClient as jest.Mock<ChartClient>;
|
||||
describe('ChartDataProvider', () => {
|
||||
beforeEach(() => {
|
||||
ChartClientMock.mockClear();
|
||||
|
||||
mockLoadFormData = defaultMockLoadFormData;
|
||||
mockLoadFormData.mockClear();
|
||||
mockLoadDatasource.mockClear();
|
||||
@@ -68,17 +71,11 @@ describe('ChartDataProvider', () => {
|
||||
|
||||
const props: ChartDataProviderProps = {
|
||||
formData: { ...bigNumberFormData },
|
||||
children: ({ loading, payload, error }) => (
|
||||
<div>
|
||||
{loading && <span role="status">Loading...</span>}
|
||||
{payload && <pre role="contentinfo">{JSON.stringify(payload)}</pre>}
|
||||
{error && <div role="alert">{error.message}</div>}
|
||||
</div>
|
||||
),
|
||||
children: () => <div />,
|
||||
};
|
||||
|
||||
function setup(overrideProps?: Partial<ChartDataProviderProps>) {
|
||||
return render(<ChartDataProvider {...props} {...overrideProps} />);
|
||||
return shallow(<ChartDataProvider {...props} {...overrideProps} />);
|
||||
}
|
||||
|
||||
it('instantiates a new ChartClient()', () => {
|
||||
@@ -89,7 +86,7 @@ describe('ChartDataProvider', () => {
|
||||
describe('ChartClient.loadFormData', () => {
|
||||
it('calls method on mount', () => {
|
||||
setup();
|
||||
expect(mockLoadFormData).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadFormData.mock.calls).toHaveLength(1);
|
||||
expect(mockLoadFormData.mock.calls[0][0]).toEqual({
|
||||
sliceId: props.sliceId,
|
||||
formData: props.formData,
|
||||
@@ -99,231 +96,234 @@ describe('ChartDataProvider', () => {
|
||||
it('should pass formDataRequestOptions to ChartClient.loadFormData', () => {
|
||||
const options = { host: 'override' };
|
||||
setup({ formDataRequestOptions: options });
|
||||
expect(mockLoadFormData).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadFormData.mock.calls).toHaveLength(1);
|
||||
expect(mockLoadFormData.mock.calls[0][1]).toEqual(options);
|
||||
});
|
||||
|
||||
it('calls ChartClient.loadFormData when formData or sliceId change', async () => {
|
||||
const { rerender } = setup();
|
||||
it('calls ChartClient.loadFormData when formData or sliceId change', () => {
|
||||
const wrapper = setup();
|
||||
const newProps = { sliceId: 123, formData: undefined };
|
||||
expect(mockLoadFormData).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadFormData.mock.calls).toHaveLength(1);
|
||||
|
||||
rerender(<ChartDataProvider {...props} {...newProps} />);
|
||||
expect(mockLoadFormData).toHaveBeenCalledTimes(2);
|
||||
wrapper.setProps(newProps);
|
||||
expect(mockLoadFormData.mock.calls).toHaveLength(2);
|
||||
expect(mockLoadFormData.mock.calls[1][0]).toEqual(newProps);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ChartClient.loadDatasource', () => {
|
||||
it('does not call method if loadDatasource is false', async () => {
|
||||
setup({ loadDatasource: false });
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
expect(mockLoadDatasource).not.toHaveBeenCalled();
|
||||
});
|
||||
it('does not method if loadDatasource is false', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(1);
|
||||
setup({ loadDatasource: false });
|
||||
setTimeout(() => {
|
||||
expect(mockLoadDatasource.mock.calls).toHaveLength(0);
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('calls method on mount if loadDatasource is true', async () => {
|
||||
setup({ loadDatasource: true });
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
expect(mockLoadDatasource).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadDatasource.mock.calls[0]).toEqual([
|
||||
props.formData.datasource,
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
it('calls method on mount if loadDatasource is true', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
setup({ loadDatasource: true });
|
||||
setTimeout(() => {
|
||||
expect(mockLoadDatasource.mock.calls).toHaveLength(1);
|
||||
expect(mockLoadDatasource.mock.calls[0][0]).toEqual(
|
||||
props.formData.datasource,
|
||||
);
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('should pass datasourceRequestOptions to ChartClient.loadDatasource', async () => {
|
||||
const options = { host: 'override' };
|
||||
setup({ loadDatasource: true, datasourceRequestOptions: options });
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
expect(mockLoadDatasource).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadDatasource.mock.calls[0][1]).toEqual(options);
|
||||
});
|
||||
it('should pass datasourceRequestOptions to ChartClient.loadDatasource', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const options = { host: 'override' };
|
||||
setup({ loadDatasource: true, datasourceRequestOptions: options });
|
||||
setTimeout(() => {
|
||||
expect(mockLoadDatasource.mock.calls).toHaveLength(1);
|
||||
expect(mockLoadDatasource.mock.calls[0][1]).toEqual(options);
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('calls ChartClient.loadDatasource if loadDatasource is true and formData or sliceId change', async () => {
|
||||
const { rerender } = setup({ loadDatasource: true });
|
||||
const newDatasource = 'test';
|
||||
it('calls ChartClient.loadDatasource if loadDatasource is true and formData or sliceId change', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(3);
|
||||
const newDatasource = 'test';
|
||||
const wrapper = setup({ loadDatasource: true });
|
||||
wrapper.setProps({
|
||||
formData: { datasource: newDatasource },
|
||||
sliceId: undefined,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<ChartDataProvider
|
||||
{...props}
|
||||
formData={{ ...props.formData, datasource: newDatasource }}
|
||||
loadDatasource
|
||||
/>,
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(mockLoadDatasource).toHaveBeenCalledTimes(2);
|
||||
expect(mockLoadDatasource.mock.calls[0]).toEqual([
|
||||
props.formData.datasource,
|
||||
undefined,
|
||||
]);
|
||||
expect(mockLoadDatasource.mock.calls[1]).toEqual([
|
||||
newDatasource,
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
setTimeout(() => {
|
||||
expect(mockLoadDatasource.mock.calls).toHaveLength(2);
|
||||
expect(mockLoadDatasource.mock.calls[0][0]).toEqual(
|
||||
props.formData.datasource,
|
||||
);
|
||||
expect(mockLoadDatasource.mock.calls[1][0]).toEqual(newDatasource);
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('ChartClient.loadQueryData', () => {
|
||||
it('calls method on mount', async () => {
|
||||
setup();
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
expect(mockLoadQueryData).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadQueryData.mock.calls[0]).toEqual([
|
||||
props.formData,
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
it('calls method on mount', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
setup();
|
||||
setTimeout(() => {
|
||||
expect(mockLoadQueryData.mock.calls).toHaveLength(1);
|
||||
expect(mockLoadQueryData.mock.calls[0][0]).toEqual(props.formData);
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('should pass queryDataRequestOptions to ChartClient.loadQueryData', async () => {
|
||||
const options = { host: 'override' };
|
||||
setup({ queryRequestOptions: options });
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
expect(mockLoadQueryData).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadQueryData).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
options,
|
||||
);
|
||||
});
|
||||
it('should pass queryDataRequestOptions to ChartClient.loadQueryData', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const options = { host: 'override' };
|
||||
setup({ queryRequestOptions: options });
|
||||
setTimeout(() => {
|
||||
expect(mockLoadQueryData.mock.calls).toHaveLength(1);
|
||||
expect(mockLoadQueryData.mock.calls[0][1]).toEqual(options);
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('calls ChartClient.loadQueryData when formData or sliceId change', async () => {
|
||||
const { rerender } = setup();
|
||||
const newFormData = { key: 'test' };
|
||||
it('calls ChartClient.loadQueryData when formData or sliceId change', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(3);
|
||||
const newFormData = { key: 'test' };
|
||||
const wrapper = setup();
|
||||
wrapper.setProps({ formData: newFormData, sliceId: undefined });
|
||||
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
rerender(<ChartDataProvider {...props} formData={newFormData} />);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(mockLoadQueryData).toHaveBeenCalledTimes(2);
|
||||
expect(mockLoadQueryData.mock.calls[0]).toEqual([
|
||||
props.formData,
|
||||
undefined,
|
||||
]);
|
||||
expect(mockLoadQueryData.mock.calls[1]).toEqual([newFormData, undefined]);
|
||||
});
|
||||
setTimeout(() => {
|
||||
expect(mockLoadQueryData.mock.calls).toHaveLength(2);
|
||||
expect(mockLoadQueryData.mock.calls[0][0]).toEqual(props.formData);
|
||||
expect(mockLoadQueryData.mock.calls[1][0]).toEqual(newFormData);
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('children', () => {
|
||||
it('shows loading state initially', async () => {
|
||||
mockLoadFormData.mockImplementation(() => new Promise(() => {}));
|
||||
mockLoadQueryData.mockImplementation(() => new Promise(() => {}));
|
||||
mockLoadDatasource.mockImplementation(() => new Promise(() => {}));
|
||||
it('calls children({ loading: true }) when loading', () => {
|
||||
const children = jest.fn<ReactNode, unknown[]>();
|
||||
setup({ children });
|
||||
|
||||
setup();
|
||||
await screen.findByRole('status');
|
||||
// during the first tick (before more promises resolve) loading is true
|
||||
expect(children.mock.calls).toHaveLength(1);
|
||||
expect(children.mock.calls[0][0]).toEqual({ loading: true });
|
||||
});
|
||||
|
||||
it('shows payload when loaded', async () => {
|
||||
mockLoadFormData.mockResolvedValue(props.formData);
|
||||
mockLoadQueryData.mockResolvedValue([props.formData]);
|
||||
mockLoadDatasource.mockResolvedValue(props.formData.datasource);
|
||||
it('calls children({ payload }) when loaded', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const children = jest.fn<ReactNode, unknown[]>();
|
||||
setup({ children, loadDatasource: true });
|
||||
|
||||
setup({ loadDatasource: true });
|
||||
setTimeout(() => {
|
||||
expect(children.mock.calls).toHaveLength(2);
|
||||
expect(children.mock.calls[1][0]).toEqual({
|
||||
payload: {
|
||||
formData: props.formData,
|
||||
datasource: props.formData.datasource,
|
||||
queriesData: [props.formData],
|
||||
},
|
||||
});
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
const payloadElement = await screen.findByRole('contentinfo');
|
||||
const actualPayload = JSON.parse(payloadElement.textContent || '');
|
||||
it('calls children({ error }) upon request error', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const children = jest.fn<ReactNode, unknown[]>();
|
||||
mockLoadFormData = jest.fn(() => Promise.reject(new Error('error')));
|
||||
|
||||
expect(actualPayload).toEqual({
|
||||
formData: props.formData,
|
||||
datasource: props.formData.datasource,
|
||||
queriesData: [props.formData],
|
||||
});
|
||||
});
|
||||
setup({ children });
|
||||
|
||||
it('shows error message upon request error', async () => {
|
||||
const errorMessage = 'error';
|
||||
mockLoadFormData.mockRejectedValue(new Error(errorMessage));
|
||||
setTimeout(() => {
|
||||
expect(children.mock.calls).toHaveLength(2); // loading + error
|
||||
expect(children.mock.calls[1][0]).toEqual({
|
||||
error: new Error('error'),
|
||||
});
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
setup();
|
||||
it('calls children({ error }) upon JS error', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const children = jest.fn<ReactNode, unknown[]>();
|
||||
|
||||
const errorElement = await screen.findByRole('alert');
|
||||
expect(errorElement).toHaveAttribute('role', 'alert');
|
||||
expect(errorElement).toHaveTextContent(errorMessage);
|
||||
});
|
||||
mockLoadFormData = jest.fn(() => {
|
||||
throw new Error('non-async error');
|
||||
});
|
||||
|
||||
it('shows error message upon JS error', async () => {
|
||||
mockLoadFormData.mockImplementation(() => {
|
||||
throw new Error('non-async error');
|
||||
});
|
||||
setup({ children });
|
||||
|
||||
setup();
|
||||
|
||||
const errorElement = await screen.findByRole('alert');
|
||||
expect(errorElement).toHaveAttribute('role', 'alert');
|
||||
expect(errorElement).toHaveTextContent('non-async error');
|
||||
});
|
||||
setTimeout(() => {
|
||||
expect(children.mock.calls).toHaveLength(2); // loading + error
|
||||
expect(children.mock.calls[1][0]).toEqual({
|
||||
error: new Error('non-async error'),
|
||||
});
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('callbacks', () => {
|
||||
it('calls onLoaded when loaded', async () => {
|
||||
const onLoaded = jest.fn();
|
||||
mockLoadFormData.mockResolvedValue(props.formData);
|
||||
mockLoadQueryData.mockResolvedValue([props.formData]);
|
||||
mockLoadDatasource.mockResolvedValue(props.formData.datasource);
|
||||
it('calls onLoad(payload) when loaded', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const onLoaded = jest.fn<void, unknown[]>();
|
||||
setup({ onLoaded, loadDatasource: true });
|
||||
|
||||
setup({ onLoaded, loadDatasource: true });
|
||||
setTimeout(() => {
|
||||
expect(onLoaded.mock.calls).toHaveLength(1);
|
||||
expect(onLoaded.mock.calls[0][0]).toEqual({
|
||||
formData: props.formData,
|
||||
datasource: props.formData.datasource,
|
||||
queriesData: [props.formData],
|
||||
});
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
it('calls onError(error) upon request error', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const onError = jest.fn<void, unknown[]>();
|
||||
mockLoadFormData = jest.fn(() => Promise.reject(new Error('error')));
|
||||
|
||||
expect(onLoaded).toHaveBeenCalledTimes(1);
|
||||
expect(onLoaded).toHaveBeenCalledWith({
|
||||
formData: props.formData,
|
||||
datasource: props.formData.datasource,
|
||||
queriesData: [props.formData],
|
||||
});
|
||||
});
|
||||
setup({ onError });
|
||||
setTimeout(() => {
|
||||
expect(onError.mock.calls).toHaveLength(1);
|
||||
expect(onError.mock.calls[0][0]).toEqual(new Error('error'));
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('calls onError upon request error', async () => {
|
||||
const onError = jest.fn();
|
||||
mockLoadFormData.mockRejectedValue(new Error('error'));
|
||||
it('calls onError(error) upon JS error', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const onError = jest.fn<void, unknown[]>();
|
||||
|
||||
setup({ onError });
|
||||
mockLoadFormData = jest.fn(() => {
|
||||
throw new Error('non-async error');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(onError).toHaveBeenCalledWith(new Error('error'));
|
||||
});
|
||||
|
||||
it('calls onError upon JS error', async () => {
|
||||
const onError = jest.fn();
|
||||
mockLoadFormData.mockImplementation(() => {
|
||||
throw new Error('non-async error');
|
||||
});
|
||||
|
||||
setup({ onError });
|
||||
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(onError).toHaveBeenCalledWith(new Error('non-async error'));
|
||||
});
|
||||
setup({ onError });
|
||||
setTimeout(() => {
|
||||
expect(onError.mock.calls).toHaveLength(1);
|
||||
expect(onError.mock.calls[0][0]).toEqual(
|
||||
new Error('non-async error'),
|
||||
);
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,12 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ReactElement } from 'react';
|
||||
import mockConsole, { RestoreConsole } from 'jest-mock-console';
|
||||
import { triggerResizeObserver } from 'resize-observer-polyfill';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import ErrorBoundary from 'react-error-boundary';
|
||||
|
||||
import {
|
||||
promiseTimeout,
|
||||
@@ -30,7 +28,9 @@ import {
|
||||
supersetTheme,
|
||||
ThemeProvider,
|
||||
} from '@superset-ui/core';
|
||||
import { mount as enzymeMount } from 'enzyme';
|
||||
import { WrapperProps } from '../../../src/chart/components/SuperChart';
|
||||
import NoResultsComponent from '../../../src/chart/components/NoResultsComponent';
|
||||
|
||||
import {
|
||||
ChartKeys,
|
||||
@@ -44,39 +44,45 @@ const DEFAULT_QUERIES_DATA = [
|
||||
{ data: ['foo2', 'bar2'] },
|
||||
];
|
||||
|
||||
// Fix for expect outside test block - move expectDimension into a test utility
|
||||
// Replace expectDimension function with a non-expect version
|
||||
function getDimensionText(container: HTMLElement) {
|
||||
const dimensionEl = container.querySelector('.dimension');
|
||||
return dimensionEl?.textContent || '';
|
||||
function expectDimension(
|
||||
renderedWrapper: cheerio.Cheerio,
|
||||
width: number,
|
||||
height: number,
|
||||
) {
|
||||
expect(renderedWrapper.find('.dimension').text()).toEqual(
|
||||
[width, height].join('x'),
|
||||
);
|
||||
}
|
||||
|
||||
const renderWithTheme = (component: ReactElement) =>
|
||||
render(component, {
|
||||
wrapper: ({ children }) => (
|
||||
<ThemeProvider theme={supersetTheme}>{children}</ThemeProvider>
|
||||
),
|
||||
const mount = (component: ReactElement) =>
|
||||
enzymeMount(component, {
|
||||
wrappingComponent: ThemeProvider,
|
||||
wrappingComponentProps: { theme: supersetTheme },
|
||||
});
|
||||
|
||||
describe('SuperChart', () => {
|
||||
jest.setTimeout(5000);
|
||||
|
||||
let restoreConsole: RestoreConsole;
|
||||
|
||||
// TODO: rewrite to rtl
|
||||
describe.skip('SuperChart', () => {
|
||||
const plugins = [
|
||||
new DiligentChartPlugin().configure({ key: ChartKeys.DILIGENT }),
|
||||
new BuggyChartPlugin().configure({ key: ChartKeys.BUGGY }),
|
||||
];
|
||||
|
||||
let restoreConsole: RestoreConsole;
|
||||
|
||||
beforeAll(() => {
|
||||
plugins.forEach(p => {
|
||||
p.unregister().register();
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
plugins.forEach(p => {
|
||||
p.unregister();
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
restoreConsole = mockConsole();
|
||||
triggerResizeObserver([]); // Reset any pending resize observers
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -99,16 +105,14 @@ describe('SuperChart', () => {
|
||||
|
||||
afterEach(() => {
|
||||
window.removeEventListener('error', onError);
|
||||
});
|
||||
|
||||
it('should have correct number of errors', () => {
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
expect(actualErrors).toBe(expectedErrors);
|
||||
expectedErrors = 0;
|
||||
});
|
||||
|
||||
it('renders default FallbackComponent', async () => {
|
||||
expectedErrors = 1;
|
||||
renderWithTheme(
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.BUGGY}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
@@ -116,19 +120,16 @@ describe('SuperChart', () => {
|
||||
height="200"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText('Oops! An error occurred!'),
|
||||
).toBeInTheDocument();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
wrapper.update();
|
||||
expect(wrapper.text()).toContain('Oops! An error occurred!');
|
||||
});
|
||||
|
||||
it('renders custom FallbackComponent', async () => {
|
||||
it('renders custom FallbackComponent', () => {
|
||||
expectedErrors = 1;
|
||||
const CustomFallbackComponent = jest.fn(() => (
|
||||
<div>Custom Fallback!</div>
|
||||
));
|
||||
|
||||
renderWithTheme(
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.BUGGY}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
@@ -138,13 +139,15 @@ describe('SuperChart', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Custom Fallback!')).toBeInTheDocument();
|
||||
expect(CustomFallbackComponent).toHaveBeenCalledTimes(1);
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(0);
|
||||
expect(CustomFallbackComponent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
it('call onErrorBoundary', async () => {
|
||||
it('call onErrorBoundary', () => {
|
||||
expectedErrors = 1;
|
||||
const handleError = jest.fn();
|
||||
renderWithTheme(
|
||||
mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.BUGGY}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
@@ -154,20 +157,17 @@ describe('SuperChart', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('Oops! An error occurred!');
|
||||
expect(handleError).toHaveBeenCalledTimes(1);
|
||||
return promiseTimeout(() => {
|
||||
expect(handleError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Update the test cases
|
||||
it('does not include ErrorBoundary if told so', async () => {
|
||||
it('does not include ErrorBoundary if told so', () => {
|
||||
expectedErrors = 1;
|
||||
const inactiveErrorHandler = jest.fn();
|
||||
const activeErrorHandler = jest.fn();
|
||||
renderWithTheme(
|
||||
<ErrorBoundary
|
||||
fallbackRender={() => <div>Error!</div>}
|
||||
onError={activeErrorHandler}
|
||||
>
|
||||
mount(
|
||||
// @ts-ignore
|
||||
<ErrorBoundary onError={activeErrorHandler}>
|
||||
<SuperChart
|
||||
disableErrorBoundary
|
||||
chartType={ChartKeys.BUGGY}
|
||||
@@ -179,24 +179,15 @@ describe('SuperChart', () => {
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
|
||||
await screen.findByText('Error!');
|
||||
expect(activeErrorHandler).toHaveBeenCalledTimes(1);
|
||||
expect(inactiveErrorHandler).not.toHaveBeenCalled();
|
||||
return promiseTimeout(() => {
|
||||
expect(activeErrorHandler).toHaveBeenCalledTimes(1);
|
||||
expect(inactiveErrorHandler).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Update the props tests to use className instead of data-testid
|
||||
// Helper function to find elements by class name
|
||||
const findByClassName = (container: HTMLElement, className: string) =>
|
||||
container.querySelector(`.${className}`);
|
||||
|
||||
// Update test cases
|
||||
// Update timeout for all async tests
|
||||
jest.setTimeout(10000);
|
||||
|
||||
// Update the props test to wait for component to render
|
||||
it('passes the props to renderer correctly', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
it('passes the props to renderer correctly', () => {
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
@@ -206,123 +197,15 @@ describe('SuperChart', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await promiseTimeout(() => {
|
||||
const testComponent = findByClassName(container, 'test-component');
|
||||
expect(testComponent).not.toBeNull();
|
||||
expect(testComponent).toBeInTheDocument();
|
||||
expect(getDimensionText(container)).toBe('101x118');
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 101, 118);
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to create a sized wrapper
|
||||
const createSizedWrapper = () => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.width = '300px';
|
||||
wrapper.style.height = '300px';
|
||||
wrapper.style.position = 'relative';
|
||||
wrapper.style.display = 'block';
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
// Update dimension tests to wait for resize observer
|
||||
// First, increase the timeout for all tests
|
||||
jest.setTimeout(20000);
|
||||
|
||||
// Update the waitForDimensions helper to include a retry mechanism
|
||||
// Update waitForDimensions to avoid await in loop
|
||||
const waitForDimensions = async (
|
||||
container: HTMLElement,
|
||||
expectedWidth: number,
|
||||
expectedHeight: number,
|
||||
) => {
|
||||
const maxAttempts = 5;
|
||||
const interval = 100;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let attempts = 0;
|
||||
|
||||
const checkDimension = () => {
|
||||
const testComponent = container.querySelector('.test-component');
|
||||
const dimensionEl = container.querySelector('.dimension');
|
||||
|
||||
if (!testComponent || !dimensionEl) {
|
||||
if (attempts >= maxAttempts) {
|
||||
reject(new Error('Elements not found'));
|
||||
return;
|
||||
}
|
||||
attempts += 1;
|
||||
setTimeout(checkDimension, interval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dimensionEl.textContent !== `${expectedWidth}x${expectedHeight}`) {
|
||||
if (attempts >= maxAttempts) {
|
||||
reject(new Error('Dimension mismatch'));
|
||||
return;
|
||||
}
|
||||
attempts += 1;
|
||||
setTimeout(checkDimension, interval);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
checkDimension();
|
||||
});
|
||||
};
|
||||
|
||||
// Update the resize observer trigger to ensure it's called after component mount
|
||||
it.skip('works when width and height are percent', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
debounceTime={1}
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for initial render
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
triggerResizeObserver([
|
||||
{
|
||||
contentRect: {
|
||||
width: 300,
|
||||
height: 300,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 300,
|
||||
bottom: 300,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON() {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
top: this.top,
|
||||
left: this.left,
|
||||
right: this.right,
|
||||
bottom: this.bottom,
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
};
|
||||
},
|
||||
},
|
||||
borderBoxSize: [{ blockSize: 300, inlineSize: 300 }],
|
||||
contentBoxSize: [{ blockSize: 300, inlineSize: 300 }],
|
||||
devicePixelContentBoxSize: [{ blockSize: 300, inlineSize: 300 }],
|
||||
target: document.createElement('div'),
|
||||
},
|
||||
]);
|
||||
|
||||
await waitForDimensions(container, 300, 300);
|
||||
});
|
||||
|
||||
it('passes the props with multiple queries to renderer correctly', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
it('passes the props with multiple queries to renderer correctly', () => {
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={DEFAULT_QUERIES_DATA}
|
||||
@@ -332,25 +215,42 @@ describe('SuperChart', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await promiseTimeout(() => {
|
||||
const testComponent = container.querySelector('.test-component');
|
||||
expect(testComponent).not.toBeNull();
|
||||
expect(testComponent).toBeInTheDocument();
|
||||
expect(getDimensionText(container)).toBe('101x118');
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 101, 118);
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the props with multiple queries and single query to renderer correctly (backward compatibility)', () => {
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={DEFAULT_QUERIES_DATA}
|
||||
width={101}
|
||||
height={118}
|
||||
formData={{ abc: 1 }}
|
||||
/>,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 101, 118);
|
||||
});
|
||||
});
|
||||
|
||||
describe('supports NoResultsComponent', () => {
|
||||
it('renders NoResultsComponent when queriesData is missing', () => {
|
||||
renderWithTheme(
|
||||
const wrapper = mount(
|
||||
<SuperChart chartType={ChartKeys.DILIGENT} width="200" height="200" />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('No Results')).toBeInTheDocument();
|
||||
expect(wrapper.find(NoResultsComponent)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders NoResultsComponent when queriesData data is null', () => {
|
||||
renderWithTheme(
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[{ data: null }]}
|
||||
@@ -359,12 +259,116 @@ describe('SuperChart', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('No Results')).toBeInTheDocument();
|
||||
expect(wrapper.find(NoResultsComponent)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('supports dynamic width and/or height', () => {
|
||||
// Add MyWrapper component definition
|
||||
it('works with width and height that are numbers', () => {
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
width={100}
|
||||
height={100}
|
||||
/>,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 100, 100);
|
||||
});
|
||||
});
|
||||
it('works when width and height are percent', () => {
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
debounceTime={1}
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>,
|
||||
);
|
||||
triggerResizeObserver();
|
||||
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 300, 300);
|
||||
}, 100);
|
||||
});
|
||||
it('works when only width is percent', () => {
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
debounceTime={1}
|
||||
width="50%"
|
||||
height="125"
|
||||
/>,
|
||||
);
|
||||
// @ts-ignore
|
||||
triggerResizeObserver([{ contentRect: { height: 125, width: 150 } }]);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
const boundingBox = renderedWrapper
|
||||
.find('div.test-component')
|
||||
.parent()
|
||||
.parent()
|
||||
.parent();
|
||||
expect(boundingBox.css('width')).toEqual('50%');
|
||||
expect(boundingBox.css('height')).toEqual('125px');
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 150, 125);
|
||||
}, 100);
|
||||
});
|
||||
it('works when only height is percent', () => {
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
debounceTime={1}
|
||||
width="50"
|
||||
height="25%"
|
||||
/>,
|
||||
);
|
||||
// @ts-ignore
|
||||
triggerResizeObserver([{ contentRect: { height: 75, width: 50 } }]);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
const boundingBox = renderedWrapper
|
||||
.find('div.test-component')
|
||||
.parent()
|
||||
.parent()
|
||||
.parent();
|
||||
expect(boundingBox.css('width')).toEqual('50px');
|
||||
expect(boundingBox.css('height')).toEqual('25%');
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 50, 75);
|
||||
}, 100);
|
||||
});
|
||||
it('works when width and height are not specified', () => {
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
debounceTime={1}
|
||||
/>,
|
||||
);
|
||||
triggerResizeObserver();
|
||||
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 300, 400);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('supports Wrapper', () => {
|
||||
function MyWrapper({ width, height, children }: WrapperProps) {
|
||||
return (
|
||||
<div>
|
||||
@@ -376,81 +380,50 @@ describe('SuperChart', () => {
|
||||
);
|
||||
}
|
||||
|
||||
it('works with width and height that are numbers', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
it('works with width and height that are numbers', () => {
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
width={100}
|
||||
height={100}
|
||||
Wrapper={MyWrapper}
|
||||
/>,
|
||||
);
|
||||
|
||||
await promiseTimeout(() => {
|
||||
const testComponent = container.querySelector('.test-component');
|
||||
expect(testComponent).not.toBeNull();
|
||||
expect(testComponent).toBeInTheDocument();
|
||||
expect(getDimensionText(container)).toBe('100x100');
|
||||
});
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
expect(renderedWrapper.find('div.wrapper-insert')).toHaveLength(1);
|
||||
expect(renderedWrapper.find('div.wrapper-insert').text()).toEqual(
|
||||
'100x100',
|
||||
);
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 100, 100);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it.skip('works when width and height are percent', async () => {
|
||||
const wrapper = createSizedWrapper();
|
||||
document.body.appendChild(wrapper);
|
||||
|
||||
const { container } = renderWithTheme(
|
||||
<div style={{ width: '100%', height: '100%', position: 'absolute' }}>
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
debounceTime={1}
|
||||
width="100%"
|
||||
height="100%"
|
||||
Wrapper={MyWrapper}
|
||||
/>
|
||||
</div>,
|
||||
it('works when width and height are percent', () => {
|
||||
const wrapper = mount(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
debounceTime={1}
|
||||
width="100%"
|
||||
height="100%"
|
||||
Wrapper={MyWrapper}
|
||||
/>,
|
||||
);
|
||||
triggerResizeObserver();
|
||||
|
||||
wrapper.appendChild(container);
|
||||
|
||||
// Wait for initial render
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Trigger resize
|
||||
triggerResizeObserver([
|
||||
{
|
||||
contentRect: {
|
||||
width: 300,
|
||||
height: 300,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 300,
|
||||
bottom: 300,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON() {
|
||||
return this;
|
||||
},
|
||||
},
|
||||
borderBoxSize: [{ blockSize: 300, inlineSize: 300 }],
|
||||
contentBoxSize: [{ blockSize: 300, inlineSize: 300 }],
|
||||
devicePixelContentBoxSize: [{ blockSize: 300, inlineSize: 300 }],
|
||||
target: wrapper,
|
||||
},
|
||||
]);
|
||||
|
||||
// Wait for resize to be processed
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// Check dimensions
|
||||
const wrapperInsert = container.querySelector('.wrapper-insert');
|
||||
expect(wrapperInsert).not.toBeNull();
|
||||
expect(wrapperInsert).toBeInTheDocument();
|
||||
expect(wrapperInsert).toHaveTextContent('300x300');
|
||||
|
||||
await waitForDimensions(container, 300, 300);
|
||||
|
||||
document.body.removeChild(wrapper);
|
||||
}, 30000);
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
expect(renderedWrapper.find('div.wrapper-insert')).toHaveLength(1);
|
||||
expect(renderedWrapper.find('div.wrapper-insert').text()).toEqual(
|
||||
'300x300',
|
||||
);
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 300, 300);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,11 +17,16 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { ReactElement } from 'react';
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import mockConsole, { RestoreConsole } from 'jest-mock-console';
|
||||
import { ChartProps, supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
ChartProps,
|
||||
promiseTimeout,
|
||||
supersetTheme,
|
||||
SupersetTheme,
|
||||
ThemeProvider,
|
||||
} from '@superset-ui/core';
|
||||
import SuperChartCore from '../../../src/chart/components/SuperChartCore';
|
||||
import {
|
||||
ChartKeys,
|
||||
@@ -30,11 +35,25 @@ import {
|
||||
SlowChartPlugin,
|
||||
} from './MockChartPlugins';
|
||||
|
||||
const renderWithTheme = (component: ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
const Wrapper = ({
|
||||
theme,
|
||||
children,
|
||||
}: {
|
||||
theme: SupersetTheme;
|
||||
children: ReactNode;
|
||||
}) => <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||
|
||||
const styledMount = (component: ReactElement) =>
|
||||
mount(component, {
|
||||
wrappingComponent: Wrapper,
|
||||
wrappingComponentProps: {
|
||||
theme: supersetTheme,
|
||||
},
|
||||
});
|
||||
|
||||
describe('SuperChartCore', () => {
|
||||
const chartProps = new ChartProps();
|
||||
|
||||
const plugins = [
|
||||
new DiligentChartPlugin().configure({ key: ChartKeys.DILIGENT }),
|
||||
new LazyChartPlugin().configure({ key: ChartKeys.LAZY }),
|
||||
@@ -44,7 +63,6 @@ describe('SuperChartCore', () => {
|
||||
let restoreConsole: RestoreConsole;
|
||||
|
||||
beforeAll(() => {
|
||||
jest.setTimeout(30000);
|
||||
plugins.forEach(p => {
|
||||
p.unregister().register();
|
||||
});
|
||||
@@ -65,83 +83,72 @@ describe('SuperChartCore', () => {
|
||||
});
|
||||
|
||||
describe('registered charts', () => {
|
||||
it('renders registered chart', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
it('renders registered chart', () => {
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
chartProps={chartProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.test-component')).toBeInTheDocument();
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders registered chart with lazy loading', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
it('renders registered chart with lazy loading', () => {
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore chartType={ChartKeys.LAZY} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.test-component')).toBeInTheDocument();
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render if chartType is not set', async () => {
|
||||
it('does not render if chartType is not set', () => {
|
||||
// Suppress warning
|
||||
// @ts-ignore chartType is required
|
||||
const { container } = renderWithTheme(<SuperChartCore />);
|
||||
const wrapper = styledMount(<SuperChartCore />);
|
||||
|
||||
await waitFor(() => {
|
||||
const testComponent = container.querySelector('.test-component');
|
||||
expect(testComponent).not.toBeInTheDocument();
|
||||
});
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().children()).toHaveLength(0);
|
||||
}, 5);
|
||||
});
|
||||
|
||||
it('adds id to container if specified', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
it('adds id to container if specified', () => {
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore chartType={ChartKeys.DILIGENT} id="the-chart" />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const element = container.querySelector('#the-chart');
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element).toHaveAttribute('id', 'the-chart');
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().attr('id')).toEqual('the-chart');
|
||||
});
|
||||
});
|
||||
|
||||
it('adds class to container if specified', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
it('adds class to container if specified', () => {
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore chartType={ChartKeys.DILIGENT} className="the-chart" />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const element = container.querySelector('.the-chart');
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element).toHaveClass('the-chart');
|
||||
});
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.hasClass('the-chart')).toBeTruthy();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('uses overrideTransformProps when specified', async () => {
|
||||
renderWithTheme(
|
||||
it('uses overrideTransformProps when specified', () => {
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
overrideTransformProps={() => ({ message: 'hulk' })}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('hulk')).toBeInTheDocument();
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('.message').text()).toEqual('hulk');
|
||||
});
|
||||
});
|
||||
|
||||
it('uses preTransformProps when specified', async () => {
|
||||
it('uses preTransformProps when specified', () => {
|
||||
const chartPropsWithPayload = new ChartProps({
|
||||
queriesData: [{ message: 'hulk' }],
|
||||
theme: supersetTheme,
|
||||
});
|
||||
|
||||
renderWithTheme(
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
preTransformProps={() => chartPropsWithPayload}
|
||||
@@ -149,77 +156,69 @@ describe('SuperChartCore', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('hulk')).toBeInTheDocument();
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('.message').text()).toEqual('hulk');
|
||||
});
|
||||
});
|
||||
|
||||
it('uses postTransformProps when specified', async () => {
|
||||
renderWithTheme(
|
||||
it('uses postTransformProps when specified', () => {
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
postTransformProps={() => ({ message: 'hulk' })}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('hulk')).toBeInTheDocument();
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('.message').text()).toEqual('hulk');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders if chartProps is not specified', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
it('renders if chartProps is not specified', () => {
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore chartType={ChartKeys.DILIGENT} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.test-component')).toBeInTheDocument();
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render anything while waiting for Chart code to load', () => {
|
||||
const { container } = renderWithTheme(
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore chartType={ChartKeys.SLOW} />,
|
||||
);
|
||||
|
||||
const testComponent = container.querySelector('.test-component');
|
||||
expect(testComponent).not.toBeInTheDocument();
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().children()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('eventually renders after Chart is loaded', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
it('eventually renders after Chart is loaded', () => {
|
||||
// Suppress warning
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore chartType={ChartKeys.SLOW} />,
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(
|
||||
container.querySelector('.test-component'),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(1);
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
it('does not render if chartProps is null', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
it('does not render if chartProps is null', () => {
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore chartType={ChartKeys.DILIGENT} chartProps={null} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unregistered charts', () => {
|
||||
it('renders error message', async () => {
|
||||
renderWithTheme(
|
||||
it('renders error message', () => {
|
||||
const wrapper = styledMount(
|
||||
<SuperChartCore chartType="4d-pie-chart" chartProps={chartProps} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('.alert')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,11 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { reactify } from '@superset-ui/core';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RenderFuncType } from '../../../src/chart/components/reactify';
|
||||
|
||||
describe('reactify(renderFn)', () => {
|
||||
@@ -79,18 +78,14 @@ describe('reactify(renderFn)', () => {
|
||||
|
||||
it('returns a React component class', () =>
|
||||
new Promise(done => {
|
||||
render(<TestComponent />);
|
||||
const wrapper = mount(<TestComponent />);
|
||||
|
||||
expect(renderFn).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText('abc')).toBeInTheDocument();
|
||||
expect(screen.getByText('abc').parentNode).toHaveAttribute('id', 'test');
|
||||
expect(wrapper.html()).toEqual('<div id="test"><b>abc</b></div>');
|
||||
setTimeout(() => {
|
||||
expect(renderFn).toHaveBeenCalledTimes(2);
|
||||
expect(screen.getByText('def')).toBeInTheDocument();
|
||||
expect(screen.getByText('def').parentNode).toHaveAttribute(
|
||||
'id',
|
||||
'test',
|
||||
);
|
||||
expect(wrapper.html()).toEqual('<div id="test"><b>def</b></div>');
|
||||
wrapper.unmount();
|
||||
done(undefined);
|
||||
}, 20);
|
||||
}));
|
||||
@@ -124,9 +119,8 @@ describe('reactify(renderFn)', () => {
|
||||
describe('defaultProps', () => {
|
||||
it('has defaultProps if renderFn.defaultProps is defined', () => {
|
||||
expect(TheChart.defaultProps).toBe(renderFn.defaultProps);
|
||||
render(<TheChart id="test" />);
|
||||
expect(screen.getByText('ghi')).toBeInTheDocument();
|
||||
expect(screen.getByText('ghi').parentNode).toHaveAttribute('id', 'test');
|
||||
const wrapper = mount(<TheChart id="test" />);
|
||||
expect(wrapper.html()).toEqual('<div id="test"><b>ghi</b></div>');
|
||||
});
|
||||
it('does not have defaultProps if renderFn.defaultProps is not defined', () => {
|
||||
const AnotherChart = reactify(() => {});
|
||||
@@ -142,9 +136,9 @@ describe('reactify(renderFn)', () => {
|
||||
});
|
||||
it('calls willUnmount hook when it is provided', () =>
|
||||
new Promise(done => {
|
||||
const { unmount } = render(<AnotherTestComponent />);
|
||||
const wrapper = mount(<AnotherTestComponent />);
|
||||
setTimeout(() => {
|
||||
unmount();
|
||||
wrapper.unmount();
|
||||
expect(willUnmountCb).toHaveBeenCalledTimes(1);
|
||||
done(undefined);
|
||||
}, 20);
|
||||
|
||||
@@ -94,7 +94,9 @@ const config: ControlPanelConfig = {
|
||||
['Tukey', t('Tukey')],
|
||||
['Min/max (no outliers)', t('Min/max (no outliers)')],
|
||||
['2/98 percentiles', t('2/98 percentiles')],
|
||||
['5/95 percentiles', t('5/95 percentiles')],
|
||||
['9/91 percentiles', t('9/91 percentiles')],
|
||||
['10/90 percentiles', t('10/90 percentiles')],
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -36,7 +36,9 @@ export type BoxPlotFormDataWhiskerOptions =
|
||||
| 'Tukey'
|
||||
| 'Min/max (no outliers)'
|
||||
| '2/98 percentiles'
|
||||
| '9/91 percentiles';
|
||||
| '5/95 percentiles'
|
||||
| '9/91 percentiles'
|
||||
| '10/90 percentiles';
|
||||
|
||||
export type BoxPlotFormXTickLayout =
|
||||
| '45°'
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
ensureIsArray,
|
||||
GenericDataType,
|
||||
getCustomFormatter,
|
||||
getMetricLabel,
|
||||
getNumberFormatter,
|
||||
getXAxisLabel,
|
||||
isDefined,
|
||||
@@ -292,20 +291,12 @@ export default function transformProps(
|
||||
const showValueIndexesB = extractShowValueIndexes(rawSeriesB, {
|
||||
stack,
|
||||
});
|
||||
|
||||
const metricsLabels = metrics
|
||||
.map(metric => getMetricLabel(metric, undefined, undefined, verboseMap))
|
||||
.filter((label): label is string => label !== undefined);
|
||||
const metricsLabelsB = metricsB.map((metric: QueryFormMetric) =>
|
||||
getMetricLabel(metric, undefined, undefined, verboseMap),
|
||||
);
|
||||
|
||||
const { totalStackedValues, thresholdValues } = extractDataTotalValues(
|
||||
rebasedDataA,
|
||||
{
|
||||
stack,
|
||||
percentageThreshold,
|
||||
metricsLabels,
|
||||
xAxisCol: xAxisLabel,
|
||||
},
|
||||
);
|
||||
const {
|
||||
@@ -314,7 +305,7 @@ export default function transformProps(
|
||||
} = extractDataTotalValues(rebasedDataB, {
|
||||
stack: Boolean(stackB),
|
||||
percentageThreshold,
|
||||
metricsLabels: metricsLabelsB,
|
||||
xAxisCol: xAxisLabel,
|
||||
});
|
||||
|
||||
annotationLayers
|
||||
|
||||
@@ -215,18 +215,14 @@ export default function transformProps(
|
||||
) {
|
||||
xAxisLabel = verboseMap[xAxisLabel];
|
||||
}
|
||||
const metricsLabels = metrics
|
||||
.map(metric => getMetricLabel(metric, undefined, undefined, verboseMap))
|
||||
.filter((label): label is string => label !== undefined);
|
||||
const isHorizontal = orientation === OrientationType.Horizontal;
|
||||
|
||||
const { totalStackedValues, thresholdValues } = extractDataTotalValues(
|
||||
rebasedData,
|
||||
{
|
||||
stack,
|
||||
percentageThreshold,
|
||||
xAxisCol: xAxisLabel,
|
||||
legendState,
|
||||
metricsLabels,
|
||||
},
|
||||
);
|
||||
const extraMetricLabels = extractExtraMetrics(chartProps.rawFormData).map(
|
||||
@@ -300,6 +296,7 @@ export default function transformProps(
|
||||
const entryName = String(entry.name || '');
|
||||
const seriesName = inverted[entryName] || entryName;
|
||||
const colorScaleKey = getOriginalSeries(seriesName, array);
|
||||
|
||||
const transformedSeries = transformSeries(
|
||||
entry,
|
||||
colorScale,
|
||||
@@ -625,7 +622,6 @@ export default function transformProps(
|
||||
theme,
|
||||
zoomable,
|
||||
legendState,
|
||||
padding,
|
||||
),
|
||||
data: legendData as string[],
|
||||
},
|
||||
|
||||
@@ -230,7 +230,7 @@ const tooltipPercentageControl: ControlSetItem = {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show percentage'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
default: true,
|
||||
description: t('Whether to display the percentage value in the tooltip'),
|
||||
visibility: ({ controls, form_data }: ControlPanelsContainerProps) =>
|
||||
Boolean(controls?.rich_tooltip?.value) &&
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
TimeFormatter,
|
||||
ValueFormatter,
|
||||
} from '@superset-ui/core';
|
||||
import { SortSeriesType, LegendPaddingType } from '@superset-ui/chart-controls';
|
||||
import { SortSeriesType } from '@superset-ui/chart-controls';
|
||||
import { format } from 'echarts/core';
|
||||
import type { LegendComponentOption } from 'echarts/components';
|
||||
import type { SeriesOption } from 'echarts';
|
||||
@@ -60,8 +60,8 @@ export function extractDataTotalValues(
|
||||
opts: {
|
||||
stack: StackType;
|
||||
percentageThreshold: number;
|
||||
xAxisCol: string;
|
||||
legendState?: LegendState;
|
||||
metricsLabels: string[];
|
||||
},
|
||||
): {
|
||||
totalStackedValues: number[];
|
||||
@@ -69,11 +69,11 @@ export function extractDataTotalValues(
|
||||
} {
|
||||
const totalStackedValues: number[] = [];
|
||||
const thresholdValues: number[] = [];
|
||||
const { stack, percentageThreshold, legendState, metricsLabels } = opts;
|
||||
const { stack, percentageThreshold, xAxisCol, legendState } = opts;
|
||||
if (stack) {
|
||||
data.forEach(datum => {
|
||||
const values = Object.keys(datum).reduce((prev, curr) => {
|
||||
if (!metricsLabels.includes(curr)) {
|
||||
if (curr === xAxisCol) {
|
||||
return prev;
|
||||
}
|
||||
if (legendState && !legendState[curr]) {
|
||||
@@ -425,7 +425,6 @@ export function getLegendProps(
|
||||
theme: SupersetTheme,
|
||||
zoomable = false,
|
||||
legendState?: LegendState,
|
||||
padding?: LegendPaddingType,
|
||||
): LegendComponentOption | LegendComponentOption[] {
|
||||
const legend: LegendComponentOption | LegendComponentOption[] = {
|
||||
orient: [LegendOrientation.Top, LegendOrientation.Bottom].includes(
|
||||
@@ -444,30 +443,13 @@ export function getLegendProps(
|
||||
borderColor: theme.colors.grayscale.base,
|
||||
},
|
||||
};
|
||||
const MIN_LEGEND_WIDTH = 0;
|
||||
const MARGIN_GUTTER = 45;
|
||||
const getLegendWidth = (paddingWidth: number) =>
|
||||
Math.max(paddingWidth - MARGIN_GUTTER, MIN_LEGEND_WIDTH);
|
||||
|
||||
switch (orientation) {
|
||||
case LegendOrientation.Left:
|
||||
legend.left = 0;
|
||||
if (padding?.left) {
|
||||
legend.textStyle = {
|
||||
overflow: 'truncate',
|
||||
width: getLegendWidth(padding.left),
|
||||
};
|
||||
}
|
||||
break;
|
||||
case LegendOrientation.Right:
|
||||
legend.right = 0;
|
||||
legend.top = zoomable ? TIMESERIES_CONSTANTS.legendRightTopOffset : 0;
|
||||
if (padding?.right) {
|
||||
legend.textStyle = {
|
||||
overflow: 'truncate',
|
||||
width: getLegendWidth(padding.right),
|
||||
};
|
||||
}
|
||||
break;
|
||||
case LegendOrientation.Bottom:
|
||||
legend.bottom = 0;
|
||||
@@ -485,7 +467,7 @@ export function getChartPadding(
|
||||
show: boolean,
|
||||
orientation: LegendOrientation,
|
||||
margin?: string | number | null,
|
||||
padding?: LegendPaddingType,
|
||||
padding?: { top?: number; bottom?: number; left?: number; right?: number },
|
||||
isHorizontal?: boolean,
|
||||
): {
|
||||
bottom: number;
|
||||
|
||||
@@ -173,7 +173,7 @@ describe('BigNumberWithTrendline', () => {
|
||||
label: 'value',
|
||||
metric_name: 'value',
|
||||
d3format: '.2f',
|
||||
currency: { symbol: 'USD', symbolPosition: 'prefix' },
|
||||
currency: `{symbol: 'USD', symbolPosition: 'prefix' }`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -36,25 +36,15 @@ describe('EchartsTimeseries transformProps', () => {
|
||||
colorScheme: 'bnbColors',
|
||||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
metrics: ['sum__num'],
|
||||
metric: 'sum__num',
|
||||
groupby: ['foo', 'bar'],
|
||||
viz_type: 'my_viz',
|
||||
};
|
||||
const queriesData = [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
'San Francisco': 1,
|
||||
'New York': 2,
|
||||
__timestamp: 599616000000,
|
||||
sum__num: 4,
|
||||
},
|
||||
{
|
||||
'San Francisco': 3,
|
||||
'New York': 4,
|
||||
__timestamp: 599916000000,
|
||||
sum__num: 8,
|
||||
},
|
||||
{ 'San Francisco': 1, 'New York': 2, __timestamp: 599616000000 },
|
||||
{ 'San Francisco': 3, 'New York': 4, __timestamp: 599916000000 },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -74,7 +64,7 @@ describe('EchartsTimeseries transformProps', () => {
|
||||
height: 600,
|
||||
echartOptions: expect.objectContaining({
|
||||
legend: expect.objectContaining({
|
||||
data: ['sum__num', 'San Francisco', 'New York'],
|
||||
data: ['San Francisco', 'New York'],
|
||||
}),
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -111,7 +101,7 @@ describe('EchartsTimeseries transformProps', () => {
|
||||
height: 600,
|
||||
echartOptions: expect.objectContaining({
|
||||
legend: expect.objectContaining({
|
||||
data: ['sum__num', 'San Francisco', 'New York'],
|
||||
data: ['San Francisco', 'New York'],
|
||||
}),
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -156,7 +146,7 @@ describe('EchartsTimeseries transformProps', () => {
|
||||
height: 600,
|
||||
echartOptions: expect.objectContaining({
|
||||
legend: expect.objectContaining({
|
||||
data: ['sum__num', 'San Francisco', 'New York', 'My Formula'],
|
||||
data: ['San Francisco', 'New York', 'My Formula'],
|
||||
}),
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -284,7 +274,7 @@ describe('EchartsTimeseries transformProps', () => {
|
||||
expect.objectContaining({
|
||||
echartOptions: expect.objectContaining({
|
||||
legend: expect.objectContaining({
|
||||
data: ['sum__num', 'San Francisco', 'New York', 'My Line'],
|
||||
data: ['San Francisco', 'New York', 'My Line'],
|
||||
}),
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -430,7 +420,7 @@ describe('Does transformProps transform series correctly', () => {
|
||||
colorScheme: 'bnbColors',
|
||||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
metrics: ['sum__num'],
|
||||
metric: 'sum__num',
|
||||
groupby: ['foo', 'bar'],
|
||||
showValue: true,
|
||||
stack: true,
|
||||
@@ -445,28 +435,24 @@ describe('Does transformProps transform series correctly', () => {
|
||||
'New York': 2,
|
||||
Boston: 1,
|
||||
__timestamp: 599616000000,
|
||||
sum__num: 4,
|
||||
},
|
||||
{
|
||||
'San Francisco': 3,
|
||||
'New York': 4,
|
||||
Boston: 1,
|
||||
__timestamp: 599916000000,
|
||||
sum__num: 8,
|
||||
},
|
||||
{
|
||||
'San Francisco': 5,
|
||||
'New York': 8,
|
||||
Boston: 6,
|
||||
__timestamp: 600216000000,
|
||||
sum__num: 19,
|
||||
},
|
||||
{
|
||||
'San Francisco': 2,
|
||||
'New York': 7,
|
||||
Boston: 2,
|
||||
__timestamp: 600516000000,
|
||||
sum__num: 11,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -482,7 +468,7 @@ describe('Does transformProps transform series correctly', () => {
|
||||
const totalStackedValues = queriesData[0].data.reduce(
|
||||
(totals, currentStack) => {
|
||||
const total = Object.keys(currentStack).reduce((stackSum, key) => {
|
||||
if (key === '__timestamp' || key === 'sum__num') return stackSum;
|
||||
if (key === '__timestamp') return stackSum;
|
||||
return stackSum + currentStack[key as keyof typeof currentStack];
|
||||
}, 0);
|
||||
totals.push(total);
|
||||
@@ -575,6 +561,7 @@ describe('Does transformProps transform series correctly', () => {
|
||||
const expectedThresholds = totalStackedValues.map(
|
||||
total => ((formData.percentageThreshold || 0) / 100) * total,
|
||||
);
|
||||
|
||||
transformedSeries.forEach((series, seriesIndex) => {
|
||||
expect(series.label.show).toBe(true);
|
||||
series.data.forEach((value, dataIndex) => {
|
||||
@@ -589,6 +576,7 @@ describe('Does transformProps transform series correctly', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not apply percentage threshold when showValue is true and stack is false', () => {
|
||||
const updatedChartPropsConfig = {
|
||||
...chartPropsConfig,
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
calculateLowerLogTick,
|
||||
dedupSeries,
|
||||
extractGroupbyLabel,
|
||||
extractDataTotalValues,
|
||||
extractSeries,
|
||||
extractShowValueIndexes,
|
||||
extractTooltipKeys,
|
||||
@@ -1086,123 +1085,6 @@ const forecastValue = [
|
||||
},
|
||||
];
|
||||
|
||||
describe('extractDataTotalValues', () => {
|
||||
it('test_extractDataTotalValues_withStack', () => {
|
||||
const data: DataRecord[] = [
|
||||
{ metric1: 10, metric2: 20, xAxisCol: '2021-01-01' },
|
||||
{ metric1: 15, metric2: 25, xAxisCol: '2021-01-02' },
|
||||
];
|
||||
const metricsLabels = ['metric1', 'metric2'];
|
||||
const opts = {
|
||||
stack: true,
|
||||
percentageThreshold: 10,
|
||||
metricsLabels,
|
||||
};
|
||||
const result = extractDataTotalValues(data, opts);
|
||||
expect(result.totalStackedValues).toEqual([30, 40]);
|
||||
expect(result.thresholdValues).toEqual([3, 4]);
|
||||
});
|
||||
|
||||
it('should calculate total and threshold values with stack option enabled', () => {
|
||||
const data: DataRecord[] = [
|
||||
{ metric1: 10, metric2: 20, xAxisCol: '2021-01-01' },
|
||||
{ metric1: 15, metric2: 25, xAxisCol: '2021-01-02' },
|
||||
];
|
||||
const metricsLabels = ['metric1', 'metric2'];
|
||||
const opts = {
|
||||
stack: true,
|
||||
percentageThreshold: 10,
|
||||
metricsLabels,
|
||||
};
|
||||
const result = extractDataTotalValues(data, opts);
|
||||
expect(result.totalStackedValues).toEqual([30, 40]);
|
||||
expect(result.thresholdValues).toEqual([3, 4]);
|
||||
});
|
||||
|
||||
it('should handle empty data array', () => {
|
||||
const data: DataRecord[] = [];
|
||||
const metricsLabels: string[] = [];
|
||||
const opts = {
|
||||
stack: true,
|
||||
percentageThreshold: 10,
|
||||
metricsLabels,
|
||||
};
|
||||
const result = extractDataTotalValues(data, opts);
|
||||
expect(result.totalStackedValues).toEqual([]);
|
||||
expect(result.thresholdValues).toEqual([]);
|
||||
});
|
||||
|
||||
it('should calculate total and threshold values with stack option disabled', () => {
|
||||
const data: DataRecord[] = [
|
||||
{ metric1: 10, metric2: 20, xAxisCol: '2021-01-01' },
|
||||
{ metric1: 15, metric2: 25, xAxisCol: '2021-01-02' },
|
||||
];
|
||||
const metricsLabels = ['metric1', 'metric2'];
|
||||
const opts = {
|
||||
stack: false,
|
||||
percentageThreshold: 10,
|
||||
metricsLabels,
|
||||
};
|
||||
const result = extractDataTotalValues(data, opts);
|
||||
expect(result.totalStackedValues).toEqual([]);
|
||||
expect(result.thresholdValues).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle data with null or undefined values', () => {
|
||||
const data: DataRecord[] = [
|
||||
{ my_x_axis: 'abc', x: 1, y: 0, z: 2 },
|
||||
{ my_x_axis: 'foo', x: null, y: 10, z: 5 },
|
||||
{ my_x_axis: null, x: 4, y: 3, z: 7 },
|
||||
];
|
||||
const metricsLabels = ['x', 'y', 'z'];
|
||||
const opts = {
|
||||
stack: true,
|
||||
percentageThreshold: 10,
|
||||
metricsLabels,
|
||||
};
|
||||
const result = extractDataTotalValues(data, opts);
|
||||
expect(result.totalStackedValues).toEqual([3, 15, 14]);
|
||||
expect(result.thresholdValues).toEqual([
|
||||
0.30000000000000004, 1.5, 1.4000000000000001,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle different percentage thresholds', () => {
|
||||
const data: DataRecord[] = [
|
||||
{ metric1: 10, metric2: 20, xAxisCol: '2021-01-01' },
|
||||
{ metric1: 15, metric2: 25, xAxisCol: '2021-01-02' },
|
||||
];
|
||||
const metricsLabels = ['metric1', 'metric2'];
|
||||
const opts = {
|
||||
stack: true,
|
||||
percentageThreshold: 50,
|
||||
metricsLabels,
|
||||
};
|
||||
const result = extractDataTotalValues(data, opts);
|
||||
expect(result.totalStackedValues).toEqual([30, 40]);
|
||||
expect(result.thresholdValues).toEqual([15, 20]);
|
||||
});
|
||||
it('should not add datum not in metrics to the total value when stacked', () => {
|
||||
const data: DataRecord[] = [
|
||||
{ xAxisCol: 'foo', xAxisSort: 10, val: 345 },
|
||||
{ xAxisCol: 'bar', xAxisSort: 20, val: 2432 },
|
||||
{ xAxisCol: 'baz', xAxisSort: 30, val: 4543 },
|
||||
];
|
||||
const metricsLabels = ['val'];
|
||||
const opts = {
|
||||
stack: true,
|
||||
percentageThreshold: 50,
|
||||
metricsLabels,
|
||||
};
|
||||
|
||||
const result = extractDataTotalValues(data, opts);
|
||||
|
||||
// Assuming extractDataTotalValues returns the total value
|
||||
// without including the 'xAxisCol' category
|
||||
expect(result.totalStackedValues).toEqual([345, 2432, 4543]); // 10 + 20, excluding the 'xAxisCol' category
|
||||
});
|
||||
});
|
||||
|
||||
test('extractTooltipKeys with rich tooltip', () => {
|
||||
const result = extractTooltipKeys(forecastValue, 1, true, false);
|
||||
expect(result).toEqual(['foo', 'bar']);
|
||||
|
||||
@@ -73,26 +73,3 @@ more details.
|
||||
└── types
|
||||
└── external.d.ts
|
||||
```
|
||||
|
||||
### Available Handlebars Helpers in Superset
|
||||
|
||||
Below, you will find a list of all currently registered helpers in the Handlebars plugin for Superset. These helpers are registered and managed in the file [`HandlebarsViewer.tsx`](./path/to/HandlebarsViewer.tsx).
|
||||
|
||||
#### List of Registered Helpers:
|
||||
|
||||
1. **`dateFormat`**: Formats a date using a specified format.
|
||||
|
||||
- **Usage**: `{{dateFormat my_date format="MMMM YYYY"}}`
|
||||
- **Default format**: `YYYY-MM-DD`.
|
||||
|
||||
2. **`stringify`**: Converts an object into a JSON string or returns a string representation of non-object values.
|
||||
|
||||
- **Usage**: `{{stringify myObj}}`.
|
||||
|
||||
3. **`formatNumber`**: Formats a number using locale-specific formatting.
|
||||
|
||||
- **Usage**: `{{formatNumber number locale="en-US"}}`.
|
||||
- **Default locale**: `en-US`.
|
||||
|
||||
4. **`parseJson`**: Parses a JSON string into a JavaScript object.
|
||||
- **Usage**: `{{parseJson jsonString}}`.
|
||||
|
||||
@@ -99,18 +99,5 @@ Handlebars.registerHelper(
|
||||
},
|
||||
);
|
||||
|
||||
// usage: {{parseJson jsonString}}
|
||||
Handlebars.registerHelper('parseJson', (jsonString: string) => {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
error.message = `Invalid JSON string: ${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Invalid JSON string: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
Helpers.registerHelpers(Handlebars);
|
||||
HandlebarsGroupBy.register(Handlebars);
|
||||
|
||||
@@ -20,9 +20,8 @@ import {
|
||||
ControlSetItem,
|
||||
CustomControlConfig,
|
||||
sharedControls,
|
||||
InfoTooltipWithTrigger,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t, validateNonEmpty, useTheme, SafeMarkdown } from '@superset-ui/core';
|
||||
import { t, validateNonEmpty } from '@superset-ui/core';
|
||||
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
|
||||
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
|
||||
import { debounceFunc } from '../../consts';
|
||||
@@ -34,48 +33,13 @@ interface HandlebarsCustomControlProps {
|
||||
const HandlebarsTemplateControl = (
|
||||
props: CustomControlConfig<HandlebarsCustomControlProps>,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const val = String(
|
||||
props?.value ? props?.value : props?.default ? props?.default : '',
|
||||
);
|
||||
|
||||
const helperDescriptionsHeader = t(
|
||||
'Available Handlebars Helpers in Superset:',
|
||||
);
|
||||
|
||||
const helperDescriptions = [
|
||||
{ key: 'dateFormat', descKey: 'Formats a date using a specified format.' },
|
||||
{ key: 'stringify', descKey: 'Converts an object to a JSON string.' },
|
||||
{
|
||||
key: 'formatNumber',
|
||||
descKey: 'Formats a number using locale-specific formatting.',
|
||||
},
|
||||
{
|
||||
key: 'parseJson',
|
||||
descKey: 'Parses a JSON string into a JavaScript object.',
|
||||
},
|
||||
];
|
||||
|
||||
const helpersTooltipContent = `
|
||||
${helperDescriptionsHeader}
|
||||
|
||||
${helperDescriptions
|
||||
.map(({ key, descKey }) => `- **${key}**: ${t(descKey)}`)
|
||||
.join('\n')}
|
||||
`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader>
|
||||
<div>
|
||||
{props.label}
|
||||
<InfoTooltipWithTrigger
|
||||
iconsStyle={{ marginLeft: theme.gridUnit }}
|
||||
tooltip={<SafeMarkdown source={helpersTooltipContent} />}
|
||||
/>
|
||||
</div>
|
||||
</ControlHeader>
|
||||
<ControlHeader>{props.label}</ControlHeader>
|
||||
<CodeEditor
|
||||
theme="dark"
|
||||
value={val}
|
||||
@@ -101,7 +65,6 @@ export const handlebarsTemplateControlSetItem: ControlSetItem = {
|
||||
</ul>`,
|
||||
isInt: false,
|
||||
renderTrigger: true,
|
||||
valueKey: null,
|
||||
|
||||
validators: [validateNonEmpty],
|
||||
mapStateToProps: ({ controls }) => ({
|
||||
|
||||
@@ -75,7 +75,6 @@ export const styleControlSetItem: ControlSetItem = {
|
||||
description: t('CSS applied to the chart'),
|
||||
isInt: false,
|
||||
renderTrigger: true,
|
||||
valueKey: null,
|
||||
|
||||
validators: [],
|
||||
mapStateToProps: ({ controls }) => ({
|
||||
|
||||
@@ -20,7 +20,7 @@ import { AriaAttributes } from 'react';
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
||||
import 'enzyme-matchers';
|
||||
import 'jest-enzyme';
|
||||
import jQuery from 'jquery';
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
||||
|
||||
@@ -109,7 +109,6 @@ export function sleep(time: number) {
|
||||
|
||||
export * from '@testing-library/react';
|
||||
export { customRender as render };
|
||||
export { default as userEvent } from '@testing-library/user-event';
|
||||
|
||||
export async function selectOption(option: string, selectName?: string) {
|
||||
const select = screen.getByRole(
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { mount as enzymeMount } from 'enzyme';
|
||||
import { shallow as enzymeShallow, mount as enzymeMount } from 'enzyme';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { supersetTheme } from '@superset-ui/core';
|
||||
import { ReactElement } from 'react';
|
||||
@@ -26,13 +26,12 @@ type optionsType = {
|
||||
wrappingComponentProps?: any;
|
||||
wrappingComponent?: ReactElement;
|
||||
context?: any;
|
||||
newOption?: string;
|
||||
};
|
||||
|
||||
export function styledMount(
|
||||
component: ReactElement,
|
||||
options: optionsType = {},
|
||||
): any {
|
||||
) {
|
||||
return enzymeMount(component, {
|
||||
...options,
|
||||
wrappingComponent: ProviderWrapper,
|
||||
@@ -42,3 +41,17 @@ export function styledMount(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function styledShallow(
|
||||
component: ReactElement,
|
||||
options: optionsType = {},
|
||||
) {
|
||||
return enzymeShallow(component, {
|
||||
...options,
|
||||
wrappingComponent: ProviderWrapper,
|
||||
wrappingComponentProps: {
|
||||
theme: supersetTheme,
|
||||
...options?.wrappingComponentProps,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,18 +39,16 @@ export const GlobalStyles = () => (
|
||||
.echarts-tooltip[style*='visibility: hidden'] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Ant Design is applying inline z-index styles causing troubles
|
||||
// TODO: Remove z-indexes when Ant Design is fully upgraded to v5
|
||||
// Prefer vanilla Ant Design z-indexes that should work out of the box
|
||||
.ant-popover,
|
||||
.antd5-dropdown,
|
||||
.ant-dropdown,
|
||||
.ant-select-dropdown,
|
||||
.antd5-modal-wrap,
|
||||
.antd5-modal-mask,
|
||||
.antd5-picker-dropdown,
|
||||
.ant-popover,
|
||||
.antd5-popover {
|
||||
.antd5-picker-dropdown {
|
||||
z-index: ${theme.zIndex.max} !important;
|
||||
}
|
||||
|
||||
@@ -107,6 +105,13 @@ export const GlobalStyles = () => (
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.ant-dropdown-menu-sub .antd5-menu.antd5-menu-vertical {
|
||||
box-shadow: none;
|
||||
}
|
||||
.ant-dropdown-menu-submenu-title,
|
||||
.ant-dropdown-menu-item {
|
||||
line-height: 1.5em !important;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ import sinon from 'sinon';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { waitFor } from 'spec/helpers/testing-library';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import * as actions from 'src/SqlLab/actions/sqlLab';
|
||||
import { LOG_EVENT } from 'src/logger/actions';
|
||||
import {
|
||||
|
||||
@@ -20,12 +20,8 @@ import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Store } from 'redux';
|
||||
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { render, fireEvent, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
import QueryLimitSelect, {
|
||||
QueryLimitSelectProps,
|
||||
|
||||
@@ -17,13 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme, t } from '@superset-ui/core';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { styled, useTheme, t } from '@superset-ui/core';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { queryEditorSetQueryLimit } from 'src/SqlLab/actions/sqlLab';
|
||||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||
import Button from 'src/components/Button';
|
||||
|
||||
export interface QueryLimitSelectProps {
|
||||
queryEditorId: string;
|
||||
@@ -35,6 +34,28 @@ export function convertToNumWithSpaces(num: number) {
|
||||
return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ');
|
||||
}
|
||||
|
||||
const LimitSelectStyled = styled.span`
|
||||
${({ theme }) => `
|
||||
.ant-dropdown-trigger {
|
||||
align-items: center;
|
||||
color: ${theme.colors.grayscale.dark2};
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
text-decoration: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
span {
|
||||
display: inline-block;
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
&:last-of-type: {
|
||||
margin-right: ${theme.gridUnit * 4}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
function renderQueryLimit(
|
||||
maxRow: number,
|
||||
setQueryLimit: (limit: number) => void,
|
||||
@@ -73,18 +94,20 @@ const QueryLimitSelect = ({
|
||||
dispatch(queryEditorSetQueryLimit(queryEditor, updatedQueryLimit));
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownRender={() => renderQueryLimit(maxRow, setQueryLimit)}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button size="small" showMarginRight={false} type="link">
|
||||
<span>{t('LIMIT')}:</span>
|
||||
<span className="limitDropdown">
|
||||
{convertToNumWithSpaces(queryLimit)}
|
||||
</span>
|
||||
<Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<LimitSelectStyled>
|
||||
<AntdDropdown
|
||||
overlay={renderQueryLimit(maxRow, setQueryLimit)}
|
||||
trigger={['click']}
|
||||
>
|
||||
<button type="button" onClick={e => e.preventDefault()}>
|
||||
<span>{t('LIMIT')}:</span>
|
||||
<span className="limitDropdown">
|
||||
{convertToNumWithSpaces(queryLimit)}
|
||||
</span>
|
||||
<Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
|
||||
</button>
|
||||
</AntdDropdown>
|
||||
</LimitSelectStyled>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { isValidElement } from 'react';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import QueryTable from 'src/SqlLab/components/QueryTable';
|
||||
import { Provider } from 'react-redux';
|
||||
import { runningQuery, successfulQuery, user } from 'src/SqlLab/fixtures';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
|
||||
@@ -28,55 +29,27 @@ const mockedProps = {
|
||||
displayLimit: 100,
|
||||
latestQueryId: 'ryhMUZCGb',
|
||||
};
|
||||
|
||||
describe('QueryTable', () => {
|
||||
test('is valid', () => {
|
||||
expect(isValidElement(<QueryTable displayLimit={100} />)).toBe(true);
|
||||
});
|
||||
|
||||
test('is valid with props', () => {
|
||||
expect(isValidElement(<QueryTable {...mockedProps} />)).toBe(true);
|
||||
});
|
||||
|
||||
test('renders a proper table', () => {
|
||||
const mockStore = configureStore([thunk]);
|
||||
const { container } = render(<QueryTable {...mockedProps} />, {
|
||||
store: mockStore({ user }),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('listview-table')).toBeVisible();
|
||||
expect(screen.getByRole('table')).toBeVisible();
|
||||
expect(container.querySelector('.table-condensed')).toBeVisible();
|
||||
expect(container.querySelectorAll('table > thead > tr')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('table > tbody > tr')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('renders empty table when no queries provided', () => {
|
||||
const mockStore = configureStore([thunk]);
|
||||
const { container } = render(
|
||||
<QueryTable {...{ ...mockedProps, queries: [] }} />,
|
||||
{ store: mockStore({ user }) },
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('listview-table')).toBeVisible();
|
||||
expect(screen.getByRole('table')).toBeVisible();
|
||||
expect(container.querySelector('.table-condensed')).toBeVisible();
|
||||
expect(container.querySelectorAll('table > thead > tr')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('table > tbody > tr')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('renders with custom displayLimit', () => {
|
||||
const mockStore = configureStore([thunk]);
|
||||
const customProps = {
|
||||
...mockedProps,
|
||||
displayLimit: 1,
|
||||
queries: [runningQuery], // Modify to only include one query
|
||||
};
|
||||
const { container } = render(<QueryTable {...customProps} />, {
|
||||
store: mockStore({ user }),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('listview-table')).toBeVisible();
|
||||
expect(container.querySelectorAll('table > tbody > tr')).toHaveLength(1);
|
||||
});
|
||||
test('is valid', () => {
|
||||
expect(isValidElement(<QueryTable displayLimit={100} />)).toBe(true);
|
||||
});
|
||||
test('is valid with props', () => {
|
||||
expect(isValidElement(<QueryTable {...mockedProps} />)).toBe(true);
|
||||
});
|
||||
test('renders a proper table', () => {
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({
|
||||
user,
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<QueryTable {...mockedProps} />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('listview-table')).toBeVisible(); // Presence of TableCollection
|
||||
expect(screen.getByRole('table')).toBeVisible();
|
||||
expect(container.querySelector('.table-condensed')).toBeVisible(); // Presence of TableView signature class
|
||||
expect(container.querySelectorAll('table > thead > tr')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('table > tbody > tr')).toHaveLength(2);
|
||||
});
|
||||
|
||||
@@ -143,13 +143,6 @@ const setup = (props?: any, store?: Store) =>
|
||||
});
|
||||
|
||||
describe('ResultSet', () => {
|
||||
// Add cleanup after each test
|
||||
afterEach(async () => {
|
||||
fetchMock.resetHistory();
|
||||
// Wait for any pending effects to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
test('renders a Table', async () => {
|
||||
const { getByTestId } = setup(
|
||||
mockedProps,
|
||||
@@ -164,10 +157,8 @@ describe('ResultSet', () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
const table = getByTestId('table-container');
|
||||
expect(table).toBeInTheDocument();
|
||||
});
|
||||
const table = getByTestId('table-container');
|
||||
expect(table).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render success query', async () => {
|
||||
@@ -254,7 +245,7 @@ describe('ResultSet', () => {
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(1),
|
||||
);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
test('should not call reRunQuery if no error', async () => {
|
||||
const query = queries[0];
|
||||
@@ -517,22 +508,13 @@ describe('ResultSet', () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const downloadButton = getByTestId('export-csv-button');
|
||||
expect(downloadButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const downloadButton = getByTestId('export-csv-button');
|
||||
await waitFor(() => fireEvent.click(downloadButton));
|
||||
|
||||
fireEvent.click(downloadButton);
|
||||
const warningModal = await findByRole('dialog');
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(warningModal).getByText(`Download is on the way`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
}, 20000);
|
||||
expect(
|
||||
within(warningModal).getByText(`Download is on the way`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not allow download as CSV when user does not have permission to export data', async () => {
|
||||
const { queryByTestId } = setup(
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import SaveDatasetActionButton from 'src/SqlLab/components/SaveDatasetActionButton';
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, useTheme } from '@superset-ui/core';
|
||||
import { FC } from 'react';
|
||||
import { t, useTheme, styled } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { DropdownButton } from 'src/components/DropdownButton';
|
||||
import Button from 'src/components/Button';
|
||||
import { DropdownButtonProps } from 'antd/lib/dropdown';
|
||||
|
||||
interface SaveDatasetActionButtonProps {
|
||||
setShowSave: (arg0: boolean) => void;
|
||||
@@ -32,14 +34,34 @@ const SaveDatasetActionButton = ({
|
||||
}: SaveDatasetActionButtonProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const StyledDropdownButton = styled(
|
||||
DropdownButton as FC<DropdownButtonProps>,
|
||||
)`
|
||||
&.ant-dropdown-button button.ant-btn.ant-btn-default {
|
||||
font-weight: ${theme.gridUnit * 150};
|
||||
background-color: ${theme.colors.primary.light4};
|
||||
color: ${theme.colors.primary.dark1};
|
||||
&:nth-of-type(2) {
|
||||
&:before,
|
||||
&:hover:before {
|
||||
border-left: 2px solid ${theme.colors.primary.dark2};
|
||||
}
|
||||
}
|
||||
}
|
||||
span[name='caret-down'] {
|
||||
margin-left: ${theme.gridUnit * 1}px;
|
||||
color: ${theme.colors.primary.dark2};
|
||||
}
|
||||
`;
|
||||
|
||||
return !overlayMenu ? (
|
||||
<Button onClick={() => setShowSave(true)} buttonStyle="primary">
|
||||
{t('Save')}
|
||||
</Button>
|
||||
) : (
|
||||
<DropdownButton
|
||||
<StyledDropdownButton
|
||||
onClick={() => setShowSave(true)}
|
||||
dropdownRender={() => overlayMenu}
|
||||
overlay={overlayMenu}
|
||||
icon={
|
||||
<Icons.CaretDown
|
||||
iconColor={theme.colors.grayscale.light5}
|
||||
@@ -49,7 +71,7 @@ const SaveDatasetActionButton = ({
|
||||
trigger={['click']}
|
||||
>
|
||||
{t('Save')}
|
||||
</DropdownButton>
|
||||
</StyledDropdownButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
*/
|
||||
import * as reactRedux from 'react-redux';
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
cleanup,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { createDatasource } from 'src/SqlLab/actions/sqlLab';
|
||||
|
||||
@@ -96,36 +96,32 @@ interface SaveDatasetModalProps {
|
||||
}
|
||||
|
||||
const Styles = styled.div`
|
||||
${({ theme }) => `
|
||||
.sdm-body {
|
||||
margin: 0 ${theme.gridUnit * 2}px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.sdm-input {
|
||||
margin-left: ${theme.gridUnit * 10}px;
|
||||
margin-left: 45px;
|
||||
width: 401px;
|
||||
}
|
||||
.sdm-autocomplete {
|
||||
width: 401px;
|
||||
align-self: center;
|
||||
margin-left: ${theme.gridUnit}px;
|
||||
}
|
||||
.sdm-radio {
|
||||
display: block;
|
||||
height: 30px;
|
||||
margin: 10px 0px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.sdm-radio span {
|
||||
display: inline-flex;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.sdm-overwrite-msg {
|
||||
margin: ${theme.gridUnit * 2}px;
|
||||
margin: 7px;
|
||||
}
|
||||
.sdm-overwrite-container {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
const updateDataset = async (
|
||||
dbId: number,
|
||||
datasetId: number,
|
||||
|
||||
@@ -18,12 +18,8 @@
|
||||
*/
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import SaveQuery from 'src/SqlLab/components/SaveQuery';
|
||||
import { initialState, databases } from 'src/SqlLab/fixtures';
|
||||
|
||||
|
||||
@@ -26,13 +26,9 @@ import {
|
||||
ThemeProvider,
|
||||
isFeatureEnabled,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
act,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { render, screen, act, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ShareSqlLabQuery from 'src/SqlLab/components/ShareSqlLabQuery';
|
||||
import { initialState } from 'src/SqlLab/fixtures';
|
||||
|
||||
@@ -137,7 +133,7 @@ describe('ShareSqlLabQuery', () => {
|
||||
});
|
||||
});
|
||||
const button = screen.getByRole('button');
|
||||
const { id: _id, remoteId: _remoteId, ...expected } = mockQueryEditor;
|
||||
const { id, remoteId, ...expected } = mockQueryEditor;
|
||||
userEvent.click(button);
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
|
||||
@@ -154,7 +150,7 @@ describe('ShareSqlLabQuery', () => {
|
||||
});
|
||||
});
|
||||
const button = screen.getByRole('button');
|
||||
const { id: _id, ...expected } = unsavedQueryEditor;
|
||||
const { id, ...expected } = unsavedQueryEditor;
|
||||
userEvent.click(button);
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, waitFor, within } from 'spec/helpers/testing-library';
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import SouthPane from 'src/SqlLab/components/SouthPane';
|
||||
import '@testing-library/jest-dom';
|
||||
import { STATUS_OPTIONS } from 'src/SqlLab/constants';
|
||||
import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
import { denormalizeTimestamp } from '@superset-ui/core';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const mockedProps = {
|
||||
queryEditorId: defaultQueryEditor.id,
|
||||
@@ -49,14 +49,12 @@ const mockState = {
|
||||
tables: [
|
||||
{
|
||||
...table,
|
||||
id: 't3',
|
||||
name: 'table3',
|
||||
dataPreviewQueryId: '2g2_iRFMl',
|
||||
queryEditorId: defaultQueryEditor.id,
|
||||
},
|
||||
{
|
||||
...table,
|
||||
id: 't4',
|
||||
name: 'table4',
|
||||
dataPreviewQueryId: 'erWdqEWPm',
|
||||
queryEditorId: defaultQueryEditor.id,
|
||||
@@ -151,22 +149,3 @@ test('should render tabs for table metadata view', () => {
|
||||
expect(tabs[index + 2]).toHaveTextContent(`${schema}.${name}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('should remove tab', async () => {
|
||||
const { getAllByRole } = await render(<SouthPane {...mockedProps} />, {
|
||||
useRedux: true,
|
||||
initialState: mockState,
|
||||
});
|
||||
|
||||
const tabs = getAllByRole('tab');
|
||||
const totalTabs = mockState.sqlLab.tables.length + 2;
|
||||
expect(tabs).toHaveLength(totalTabs);
|
||||
const removeButton = within(tabs[2].parentElement as HTMLElement).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: /remove/,
|
||||
},
|
||||
);
|
||||
userEvent.click(removeButton);
|
||||
await waitFor(() => expect(getAllByRole('tab')).toHaveLength(totalTabs - 1));
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ const SouthPane = ({
|
||||
dispatch(removeTables([table]));
|
||||
}
|
||||
},
|
||||
[dispatch, pinnedTables],
|
||||
[dispatch, queryEditorId],
|
||||
);
|
||||
|
||||
return offline ? (
|
||||
|
||||
@@ -17,18 +17,13 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { FocusEventHandler } from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
isFeatureEnabled,
|
||||
getExtensionsRegistry,
|
||||
FeatureFlag,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
act,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import reducers from 'spec/helpers/reducerIndex';
|
||||
import { setupStore } from 'src/views/store';
|
||||
@@ -140,15 +135,6 @@ const createStore = (initState: object) =>
|
||||
});
|
||||
|
||||
describe('SqlEditor', () => {
|
||||
beforeAll(() => {
|
||||
jest.setTimeout(30000);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
cleanup();
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
const mockedProps = {
|
||||
queryEditor: initialState.sqlLab.queryEditors[0],
|
||||
tables: [table],
|
||||
@@ -201,27 +187,16 @@ describe('SqlEditor', () => {
|
||||
});
|
||||
|
||||
it('render a SqlEditorLeftBar', async () => {
|
||||
const { getByTestId, unmount } = setup(mockedProps, store);
|
||||
|
||||
await waitFor(
|
||||
() => expect(getByTestId('mock-sql-editor-left-bar')).toBeInTheDocument(),
|
||||
{ timeout: 10000 },
|
||||
const { getByTestId } = setup(mockedProps, store);
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('mock-sql-editor-left-bar')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
unmount();
|
||||
}, 15000);
|
||||
|
||||
// Update other similar tests with timeouts
|
||||
it('render an AceEditorWrapper', async () => {
|
||||
const { findByTestId, unmount } = setup(mockedProps, store);
|
||||
|
||||
await waitFor(
|
||||
() => expect(findByTestId('react-ace')).resolves.toBeInTheDocument(),
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
unmount();
|
||||
}, 15000);
|
||||
const { findByTestId } = setup(mockedProps, store);
|
||||
expect(await findByTestId('react-ace')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('skip rendering an AceEditorWrapper when the current tab is inactive', async () => {
|
||||
const { findByTestId, queryByTestId } = setup(
|
||||
|
||||
@@ -56,8 +56,7 @@ import Mousetrap from 'mousetrap';
|
||||
import Button from 'src/components/Button';
|
||||
import Timer from 'src/components/Timer';
|
||||
import ResizableSidebar from 'src/components/ResizableSidebar';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Skeleton } from 'src/components';
|
||||
import { AntdDropdown, Skeleton } from 'src/components';
|
||||
import { Switch } from 'src/components/Switch';
|
||||
import { Input } from 'src/components/Input';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
@@ -869,14 +868,9 @@ const SqlEditor: FC<Props> = ({
|
||||
<span>
|
||||
<ShareSqlLabQuery queryEditorId={queryEditor.id} />
|
||||
</span>
|
||||
<Dropdown
|
||||
dropdownRender={() => renderDropdown()}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button buttonSize="xsmall" type="link" showMarginRight={false}>
|
||||
<Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<AntdDropdown overlay={renderDropdown()} trigger={['click']}>
|
||||
<Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
|
||||
</AntdDropdown>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -17,13 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
import fetchMock from 'fetch-mock';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
within,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import SqlEditorLeftBar, {
|
||||
SqlEditorLeftBarProps,
|
||||
} from 'src/SqlLab/components/SqlEditorLeftBar';
|
||||
|
||||
@@ -22,9 +22,9 @@ import {
|
||||
fireEvent,
|
||||
screen,
|
||||
render,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryEditor } from 'src/SqlLab/types';
|
||||
import {
|
||||
initialState,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useMemo, FC } from 'react';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
|
||||
import { MenuDotsDropdown } from 'src/components/Dropdown';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { styled, t, QueryState } from '@superset-ui/core';
|
||||
import {
|
||||
@@ -88,10 +88,10 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
|
||||
|
||||
return (
|
||||
<TabTitleWrapper>
|
||||
<MenuDotsDropdown
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu style={{ width: 176 }}>
|
||||
<Menu.Item
|
||||
className="close-btn"
|
||||
key="1"
|
||||
|
||||
@@ -122,7 +122,7 @@ test('fades table', async () => {
|
||||
'1',
|
||||
),
|
||||
);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
test('sorts columns', async () => {
|
||||
const { getAllByTestId, getByText } = render(
|
||||
|
||||
@@ -30,8 +30,11 @@ import {
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import Icons from 'src/components/Icons';
|
||||
import type { SqlLabRootState } from 'src/SqlLab/types';
|
||||
import { Skeleton, AntdBreadcrumb as Breadcrumb, Button } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import {
|
||||
Skeleton,
|
||||
AntdBreadcrumb as Breadcrumb,
|
||||
AntdDropdown,
|
||||
} from 'src/components';
|
||||
import FilterableTable from 'src/components/FilterableTable';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import {
|
||||
@@ -305,8 +308,8 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
|
||||
<Title>
|
||||
<Icons.Table iconSize="l" />
|
||||
{tableName}
|
||||
<Dropdown
|
||||
dropdownRender={() => (
|
||||
<AntdDropdown
|
||||
overlay={
|
||||
<Menu
|
||||
onClick={({ key }) => {
|
||||
if (key === 'refresh-table') {
|
||||
@@ -321,17 +324,15 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
|
||||
}}
|
||||
items={dropdownMenu}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button buttonSize="xsmall" type="link">
|
||||
<Icons.DownSquareOutlined
|
||||
iconSize="m"
|
||||
style={{ marginTop: 2, marginLeft: 4 }}
|
||||
aria-label={t('Table actions')}
|
||||
/>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Icons.DownSquareOutlined
|
||||
iconSize="m"
|
||||
style={{ marginTop: 2, marginLeft: 4 }}
|
||||
aria-label={t('Table actions')}
|
||||
/>
|
||||
</AntdDropdown>
|
||||
</Title>
|
||||
{isMetadataRefreshing ? (
|
||||
<Skeleton active />
|
||||
|
||||
@@ -16,12 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Alert, { AlertProps } from 'src/components/Alert';
|
||||
|
||||
type AlertType = Pick<AlertProps, 'type'>;
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import AlteredSliceTag, {
|
||||
alterForComparison,
|
||||
formatValueHandler,
|
||||
|
||||
@@ -16,12 +16,9 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import '@testing-library/jest-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { ModifiedInfo } from '.';
|
||||
|
||||
@@ -43,7 +40,7 @@ test('should render a tooltip when user is provided', async () => {
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(screen.getByText('Modified by: Foo Bar')).toBeInTheDocument();
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
test('should render only the date if username is not provided', async () => {
|
||||
render(<ModifiedInfo date={TEST_DATE} />);
|
||||
|
||||
@@ -67,7 +67,7 @@ const decideType = (buttonStyle: ButtonStyle) => {
|
||||
success: 'primary',
|
||||
secondary: 'default',
|
||||
default: 'default',
|
||||
tertiary: 'default',
|
||||
tertiary: 'dashed',
|
||||
dashed: 'dashed',
|
||||
link: 'link',
|
||||
};
|
||||
|
||||
@@ -16,17 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, waitFor } from 'spec/helpers/testing-library';
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import Card from '.';
|
||||
|
||||
afterEach(async () => {
|
||||
// Wait for any pending effects to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
test('should render', async () => {
|
||||
test('should render', () => {
|
||||
const { container } = render(<Card />);
|
||||
await waitFor(() => {
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -16,12 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import CertifiedBadge, {
|
||||
CertifiedBadgeProps,
|
||||
} from 'src/components/CertifiedBadge';
|
||||
|
||||
@@ -30,7 +30,6 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
Behavior,
|
||||
BinaryQueryObjectFilterClause,
|
||||
Column,
|
||||
ContextMenuFilters,
|
||||
ensureIsArray,
|
||||
FeatureFlag,
|
||||
@@ -43,11 +42,8 @@ import {
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { usePermissions } from 'src/hooks/usePermissions';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { AntdDropdown as Dropdown } from 'src/components/index';
|
||||
import { updateDataMask } from 'src/dataMask/actions';
|
||||
import DrillByModal from 'src/components/Chart/DrillBy/DrillByModal';
|
||||
import { useVerboseMap } from 'src/hooks/apiResources/datasets';
|
||||
import { Dataset } from 'src/components/Chart/types';
|
||||
import { DrillDetailMenuItems } from '../DrillDetail';
|
||||
import { getMenuAdjustedY } from '../utils';
|
||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||
@@ -118,22 +114,8 @@ const ChartContextMenu = (
|
||||
}>({ clientX: 0, clientY: 0 });
|
||||
|
||||
const [drillModalIsOpen, setDrillModalIsOpen] = useState(false);
|
||||
const [drillByColumn, setDrillByColumn] = useState<Column>();
|
||||
const [showDrillByModal, setShowDrillByModal] = useState(false);
|
||||
const [dataset, setDataset] = useState<Dataset>();
|
||||
const verboseMap = useVerboseMap(dataset);
|
||||
|
||||
const handleDrillBy = useCallback((column: Column, dataset: Dataset) => {
|
||||
setDrillByColumn(column);
|
||||
setDataset(dataset); // Save dataset when drilling
|
||||
setShowDrillByModal(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseDrillByModal = useCallback(() => {
|
||||
setShowDrillByModal(false);
|
||||
}, []);
|
||||
|
||||
const menuItems: React.JSX.Element[] = [];
|
||||
const menuItems = [];
|
||||
|
||||
const showDrillToDetail =
|
||||
isFeatureEnabled(FeatureFlag.DrillToDetail) &&
|
||||
@@ -267,9 +249,9 @@ const ChartContextMenu = (
|
||||
formData={formData}
|
||||
contextMenuY={clientY}
|
||||
submenuIndex={submenuIndex}
|
||||
canDownload={canDownload}
|
||||
open={openKeys.includes('drill-by-submenu')}
|
||||
key="drill-by-submenu"
|
||||
onDrillBy={handleDrillBy}
|
||||
{...(additionalConfig?.drillBy || {})}
|
||||
/>,
|
||||
);
|
||||
@@ -304,7 +286,7 @@ const ChartContextMenu = (
|
||||
return ReactDOM.createPortal(
|
||||
<>
|
||||
<Dropdown
|
||||
dropdownRender={() => (
|
||||
overlay={
|
||||
<Menu
|
||||
className="chart-context-menu"
|
||||
data-test="chart-context-menu"
|
||||
@@ -320,15 +302,15 @@ const ChartContextMenu = (
|
||||
<Menu.Item disabled>{t('No actions')}</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
)}
|
||||
}
|
||||
trigger={['click']}
|
||||
onOpenChange={value => {
|
||||
onVisibleChange={value => {
|
||||
setVisible(value);
|
||||
if (!value) {
|
||||
setOpenKeys([]);
|
||||
}
|
||||
}}
|
||||
open={visible}
|
||||
visible={visible}
|
||||
>
|
||||
<span
|
||||
id={`hidden-span-${id}`}
|
||||
@@ -353,16 +335,6 @@ const ChartContextMenu = (
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showDrillByModal && drillByColumn && dataset && filters?.drillBy && (
|
||||
<DrillByModal
|
||||
column={drillByColumn}
|
||||
drillByConfig={filters?.drillBy}
|
||||
formData={formData}
|
||||
onHideModal={handleCloseDrillByModal}
|
||||
dataset={{ ...dataset!, verbose_map: verboseMap }}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
)}
|
||||
</>,
|
||||
document.body,
|
||||
);
|
||||
|
||||
@@ -16,19 +16,14 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
Behavior,
|
||||
ChartMetadata,
|
||||
getChartMetadataRegistry,
|
||||
} from '@superset-ui/core';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
within,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { render, screen, within, waitFor } from 'spec/helpers/testing-library';
|
||||
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { supersetGetCache } from 'src/utils/cachedSupersetGet';
|
||||
@@ -79,6 +74,7 @@ const renderMenu = ({
|
||||
<DrillByMenuItems
|
||||
formData={formData ?? defaultFormData}
|
||||
drillByConfig={drillByConfig}
|
||||
canDownload
|
||||
open
|
||||
{...rest}
|
||||
/>
|
||||
@@ -168,9 +164,6 @@ test('render menu item with submenu without searchbox', async () => {
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Add global timeout for all tests
|
||||
jest.setTimeout(20000);
|
||||
|
||||
test('render menu item with submenu and searchbox', async () => {
|
||||
fetchMock.get(DATASET_ENDPOINT, {
|
||||
result: { columns: defaultColumns },
|
||||
@@ -178,32 +171,18 @@ test('render menu item with submenu and searchbox', async () => {
|
||||
renderMenu({});
|
||||
await waitFor(() => fetchMock.called(DATASET_ENDPOINT));
|
||||
await expectDrillByEnabled();
|
||||
defaultColumns.forEach(column => {
|
||||
expect(screen.getByText(column.column_name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Wait for all columns to be visible
|
||||
await waitFor(
|
||||
() => {
|
||||
defaultColumns.forEach(column => {
|
||||
expect(screen.getByText(column.column_name)).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
const searchbox = await waitFor(
|
||||
() => screen.getAllByPlaceholderText('Search columns')[1],
|
||||
);
|
||||
const searchbox = screen.getAllByPlaceholderText('Search columns')[1];
|
||||
expect(searchbox).toBeInTheDocument();
|
||||
|
||||
userEvent.type(searchbox, 'col1');
|
||||
|
||||
const expectedFilteredColumnNames = ['col1', 'col10', 'col11'];
|
||||
await screen.findByText('col1');
|
||||
|
||||
// Wait for filtered results
|
||||
await waitFor(() => {
|
||||
expectedFilteredColumnNames.forEach(colName => {
|
||||
expect(screen.getByText(colName)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
const expectedFilteredColumnNames = ['col1', 'col10', 'col11'];
|
||||
|
||||
defaultColumns
|
||||
.filter(col => !expectedFilteredColumnNames.includes(col.column_name))
|
||||
@@ -231,22 +210,16 @@ test('Do not display excluded column in the menu', async () => {
|
||||
await waitFor(() => fetchMock.called(DATASET_ENDPOINT));
|
||||
await expectDrillByEnabled();
|
||||
|
||||
// Wait for menu items to be loaded
|
||||
await waitFor(
|
||||
() => {
|
||||
defaultColumns
|
||||
.filter(column => !excludedColNames.includes(column.column_name))
|
||||
.forEach(column => {
|
||||
expect(screen.getByText(column.column_name)).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
excludedColNames.forEach(colName => {
|
||||
expect(screen.queryByText(colName)).not.toBeInTheDocument();
|
||||
});
|
||||
}, 20000);
|
||||
|
||||
defaultColumns
|
||||
.filter(column => !excludedColNames.includes(column.column_name))
|
||||
.forEach(column => {
|
||||
expect(screen.getByText(column.column_name)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('When menu item is clicked, call onSelection with clicked column and drill by filters', async () => {
|
||||
fetchMock
|
||||
@@ -264,10 +237,7 @@ test('When menu item is clicked, call onSelection with clicked column and drill
|
||||
await waitFor(() => fetchMock.called(DATASET_ENDPOINT));
|
||||
await expectDrillByEnabled();
|
||||
|
||||
// Wait for col1 to be visible before clicking
|
||||
const col1Element = await waitFor(() => screen.getByText('col1'));
|
||||
userEvent.click(col1Element);
|
||||
|
||||
userEvent.click(screen.getByText('col1'));
|
||||
expect(onSelectionMock).toHaveBeenCalledWith(
|
||||
{
|
||||
column_name: 'col1',
|
||||
@@ -275,4 +245,4 @@ test('When menu item is clicked, call onSelection with clicked column and drill
|
||||
},
|
||||
{ filters: defaultFilters, groupbyFieldName: 'groupby' },
|
||||
);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
@@ -53,8 +53,10 @@ import {
|
||||
cachedSupersetGet,
|
||||
supersetGetCache,
|
||||
} from 'src/utils/cachedSupersetGet';
|
||||
import { useVerboseMap } from 'src/hooks/apiResources/datasets';
|
||||
import { InputRef } from 'antd-v5';
|
||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||
import DrillByModal from './DrillByModal';
|
||||
import { getSubmenuYOffset } from '../utils';
|
||||
import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
|
||||
import { Dataset } from '../types';
|
||||
@@ -72,8 +74,8 @@ export interface DrillByMenuItemsProps {
|
||||
onClick?: (event: MouseEvent) => void;
|
||||
openNewModal?: boolean;
|
||||
excludedColumns?: Column[];
|
||||
canDownload: boolean;
|
||||
open: boolean;
|
||||
onDrillBy?: (column: Column, dataset: Dataset) => void;
|
||||
}
|
||||
|
||||
const loadDrillByOptions = getExtensionsRegistry().get('load.drillby.options');
|
||||
@@ -104,8 +106,8 @@ export const DrillByMenuItems = ({
|
||||
onClick = () => {},
|
||||
excludedColumns,
|
||||
openNewModal = true,
|
||||
canDownload,
|
||||
open,
|
||||
onDrillBy,
|
||||
...rest
|
||||
}: DrillByMenuItemsProps) => {
|
||||
const theme = useTheme();
|
||||
@@ -115,20 +117,25 @@ export const DrillByMenuItems = ({
|
||||
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
|
||||
const [dataset, setDataset] = useState<Dataset>();
|
||||
const [columns, setColumns] = useState<Column[]>([]);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [currentColumn, setCurrentColumn] = useState();
|
||||
const ref = useRef<InputRef>(null);
|
||||
const showSearch =
|
||||
loadDrillByOptions || columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD;
|
||||
|
||||
const handleSelection = useCallback(
|
||||
(event, column) => {
|
||||
onClick(event);
|
||||
onSelection(column, drillByConfig);
|
||||
if (openNewModal && onDrillBy && dataset) {
|
||||
onDrillBy(column, dataset);
|
||||
setCurrentColumn(column);
|
||||
if (openNewModal) {
|
||||
setShowModal(true);
|
||||
}
|
||||
},
|
||||
[drillByConfig, onClick, onSelection, openNewModal, onDrillBy, dataset],
|
||||
[drillByConfig, onClick, onSelection, openNewModal],
|
||||
);
|
||||
const closeModal = useCallback(() => {
|
||||
setShowModal(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -149,6 +156,7 @@ export const DrillByMenuItems = ({
|
||||
?.behaviors.find(behavior => behavior === Behavior.DrillBy),
|
||||
[formData.viz_type],
|
||||
);
|
||||
const verboseMap = useVerboseMap(dataset);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadOptions() {
|
||||
@@ -267,11 +275,11 @@ export const DrillByMenuItems = ({
|
||||
const column = columns[index];
|
||||
return (
|
||||
<MenuItemWithTruncation
|
||||
menuKey={`drill-by-item-${column.column_name}`}
|
||||
key={`drill-by-item-${column.column_name}`}
|
||||
tooltipText={column.verbose_name || column.column_name}
|
||||
{...rest}
|
||||
onClick={e => handleSelection(e, column)}
|
||||
style={style}
|
||||
{...rest}
|
||||
>
|
||||
{column.verbose_name || column.column_name}
|
||||
</MenuItemWithTruncation>
|
||||
@@ -281,7 +289,6 @@ export const DrillByMenuItems = ({
|
||||
return (
|
||||
<>
|
||||
<Menu.SubMenu
|
||||
key="drill-by-submenu"
|
||||
title={t('Drill by')}
|
||||
popupClassName="chart-context-submenu"
|
||||
popupOffset={[0, submenuYOffset]}
|
||||
@@ -342,6 +349,16 @@ export const DrillByMenuItems = ({
|
||||
)}
|
||||
</div>
|
||||
</Menu.SubMenu>
|
||||
{showModal && (
|
||||
<DrillByModal
|
||||
column={currentColumn}
|
||||
drillByConfig={drillByConfig}
|
||||
formData={formData}
|
||||
onHideModal={closeModal}
|
||||
dataset={{ ...dataset!, verbose_map: verboseMap }}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,13 +20,9 @@
|
||||
import { useState } from 'react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { omit, omitBy } from 'lodash';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
within,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { waitFor, within } from '@testing-library/react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
|
||||
import mockState from 'spec/fixtures/mockState';
|
||||
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user