mirror of
https://github.com/apache/superset.git
synced 2026-07-01 04:15:31 +00:00
Compare commits
50 Commits
boxplot-ex
...
esbuild-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c7835a244 | ||
|
|
ad057324b7 | ||
|
|
2c583d1584 | ||
|
|
15fbb195e9 | ||
|
|
5867b87680 | ||
|
|
52563d3eea | ||
|
|
21348c418a | ||
|
|
af3589fe91 | ||
|
|
937d40cdde | ||
|
|
319a860f23 | ||
|
|
d3b854a833 | ||
|
|
650fa5ccfb | ||
|
|
db70c7912c | ||
|
|
3160607aaf | ||
|
|
eec54affc3 | ||
|
|
31d6f5a639 | ||
|
|
60424c4ccd | ||
|
|
60bbd72028 | ||
|
|
a78968c68e | ||
|
|
1c3ec21e0f | ||
|
|
8d1fb9c82d | ||
|
|
f01493277f | ||
|
|
0f6bd5ea83 | ||
|
|
0030f46d2d | ||
|
|
06f8f8e608 | ||
|
|
a144464506 | ||
|
|
2770bc0865 | ||
|
|
bcc61bd933 | ||
|
|
38c46fcafd | ||
|
|
f3e7c64de6 | ||
|
|
f9f8c5d07a | ||
|
|
c5f4a7f302 | ||
|
|
389aae270b | ||
|
|
e97eb71a52 | ||
|
|
5a8488af36 | ||
|
|
205cff3a94 | ||
|
|
649a0dec6c | ||
|
|
e8990f4a36 | ||
|
|
acf91e1f60 | ||
|
|
6ed9dae2f7 | ||
|
|
ea5879bf2b | ||
|
|
c7c3b1b0e9 | ||
|
|
c64018d421 | ||
|
|
53d944d013 | ||
|
|
9aa8b09505 | ||
|
|
8984f88a3e | ||
|
|
386aa93e24 | ||
|
|
0cd0fcdecb | ||
|
|
cde2d49c95 | ||
|
|
9e5876dc17 |
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: "20"
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
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: "20"
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
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,17 +50,45 @@ jobs:
|
||||
echo "result=up" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "result=noop" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get event SHA
|
||||
id: get-sha
|
||||
run: |
|
||||
echo "sha=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
|
||||
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);
|
||||
|
||||
- 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
|
||||
@@ -81,6 +109,7 @@ 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: |
|
||||
@@ -161,8 +190,9 @@ 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-${{ github.event.inputs.issue_number || github.event.issue.number }}-ci
|
||||
docker tag $IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:pr-$PR_NUMBER-ci
|
||||
docker push -a $ECR_REGISTRY/$ECR_REPOSITORY
|
||||
|
||||
ephemeral-env-up:
|
||||
@@ -193,11 +223,13 @@ 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-${{ github.event.inputs.issue_number || github.event.issue.number }}-ci
|
||||
--image-ids imageTag=pr-$PR_NUMBER-ci
|
||||
|
||||
- name: Fail on missing container image
|
||||
if: steps.check-image.outcome == 'failure'
|
||||
@@ -207,7 +239,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.issue.number }},
|
||||
issue_number: ${{ github.event.inputs.issue_number || github.event.pull_request.number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: errMsg
|
||||
@@ -220,7 +252,7 @@ jobs:
|
||||
with:
|
||||
task-definition: .github/workflows/ecs-task-definition.json
|
||||
container-name: superset-ci
|
||||
image: ${{ steps.login-ecr.outputs.registry }}/superset-ci:pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-ci
|
||||
image: ${{ steps.login-ecr.outputs.registry }}/superset-ci:pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-ci
|
||||
|
||||
- name: Update env vars in the Amazon ECS task definition
|
||||
run: |
|
||||
@@ -229,29 +261,30 @@ jobs:
|
||||
- name: Describe ECS service
|
||||
id: describe-services
|
||||
run: |
|
||||
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
|
||||
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${{ github.event.inputs.issue_number || github.event.pull_request.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-${{ github.event.inputs.issue_number || github.event.issue.number }}-service \
|
||||
--service-name pr-$PR_NUMBER-service \
|
||||
--task-definition superset-ci \
|
||||
--launch-type FARGATE \
|
||||
--desired-count 1 \
|
||||
--platform-version LATEST \
|
||||
--network-configuration "awsvpcConfiguration={subnets=[$ECR_SUBNETS],securityGroups=[$ECR_SECURITY_GROUP],assignPublicIp=ENABLED}" \
|
||||
--tags key=pr,value=${{ github.event.inputs.issue_number || github.event.issue.number }} key=github_user,value=${{ github.actor }}
|
||||
--tags key=pr,value=$PR_NUMBER key=github_user,value=${{ github.actor }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
id: deploy-task
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: pr-${{ github.event.inputs.issue_number || github.event.issue.number }}-service
|
||||
service: pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-service
|
||||
cluster: superset-ci
|
||||
wait-for-service-stability: true
|
||||
wait-for-minutes: 10
|
||||
@@ -259,7 +292,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.issue.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.pull_request.number }}-service | jq '.taskArns | first')" >> $GITHUB_OUTPUT
|
||||
- name: Get network interface
|
||||
id: get-eni
|
||||
run: |
|
||||
@@ -274,20 +307,22 @@ jobs:
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
const issue_number = context.payload.inputs?.issue_number || context.issue.number;
|
||||
github.rest.issues.createComment({
|
||||
issue_number: ${{ github.event.inputs.issue_number || github.event.issue.number }},
|
||||
issue_number: 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: ${{ github.event.inputs.issue_number || github.event.issue.number }},
|
||||
issue_number: 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,13 +24,7 @@ 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:
|
||||
@@ -46,11 +40,11 @@ jobs:
|
||||
git fetch --prune --unshallow
|
||||
git tag -d `git tag | grep -E '^trigger-'`
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
- name: Install Node.js
|
||||
if: env.HAS_TAGS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
|
||||
- name: Cache npm
|
||||
if: env.HAS_TAGS
|
||||
|
||||
@@ -26,7 +26,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: ["chrome"]
|
||||
node: [20]
|
||||
env:
|
||||
SUPERSET_ENV: development
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
@@ -66,7 +65,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
|
||||
@@ -28,9 +28,6 @@ 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
|
||||
@@ -41,7 +38,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- 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 20
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version-file: './docs/.nvmrc'
|
||||
- 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 20
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version-file: './docs/.nvmrc'
|
||||
- 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: "20"
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- 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: '18'
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- 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: '20'
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,6 +21,7 @@
|
||||
*.swp
|
||||
__pycache__
|
||||
|
||||
.aider*
|
||||
.local
|
||||
.cache
|
||||
.bento*
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -18,16 +18,19 @@
|
||||
######################################################################
|
||||
# Node stage to deal with static asset construction
|
||||
######################################################################
|
||||
ARG PY_VER=3.11-slim-bookworm
|
||||
ARG PY_VER=3.11.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="false" # Include translations in the final build
|
||||
ARG BUILD_TRANSLATIONS
|
||||
ENV BUILD_TRANSLATIONS=${BUILD_TRANSLATIONS}
|
||||
ARG DEV_MODE="false" # Skip frontend build in dev mode
|
||||
ENV DEV_MODE=${DEV_MODE}
|
||||
@@ -122,10 +125,13 @@ 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/docker/pip-install.sh --requires-build-essential -r requirements/translations.txt
|
||||
. /app/.venv/bin/activate && /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,6 +137,7 @@ 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
|
||||
libffi-dev python3-dev libsasl2-dev libldap2-dev libxi-dev chromium zstd
|
||||
|
||||
# Install nodejs for custom build
|
||||
# https://nodejs.org/en/download/package-manager/
|
||||
RUN set -eux; \
|
||||
curl -sL https://deb.nodesource.com/setup_18.x | bash -; \
|
||||
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
|
||||
@@ -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 \
|
||||
PYTHONPATH=/home/superset/superset/ \
|
||||
SUPERSET_TESTENV=true
|
||||
COPY from_tarball_entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@@ -29,13 +29,16 @@ 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
|
||||
RUN apt-get install -y subversion build-essential libssl-dev \
|
||||
libffi-dev python3-dev libsasl2-dev libldap2-dev libxi-dev chromium zstd
|
||||
|
||||
# Install nodejs for custom build
|
||||
# https://nodejs.org/en/download/package-manager/
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
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 mkdir -p /home/superset
|
||||
RUN chown superset /home/superset
|
||||
@@ -46,14 +49,12 @@ 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 apache-superset-$VERSION
|
||||
WORKDIR /home/superset/apache-superset-$VERSION/superset-frontend
|
||||
|
||||
RUN cd superset-frontend \
|
||||
&& npm ci \
|
||||
RUN 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 \
|
||||
@@ -62,6 +63,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
|
||||
PYTHONPATH=/home/superset/superset/
|
||||
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:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can this form post on UserInfoEditView |: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 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,7 +65,6 @@ 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|
|
||||
@@ -76,7 +75,6 @@ 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:|
|
||||
@@ -118,8 +116,6 @@ 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:|
|
||||
@@ -129,13 +125,6 @@ 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|
|
||||
@@ -192,7 +181,6 @@ 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,8 +24,9 @@ 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
|
||||
@@ -45,7 +46,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,6 +69,7 @@ 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>` |
|
||||
@@ -77,6 +78,7 @@ 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}` |
|
||||
@@ -1074,6 +1076,23 @@ 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
|
||||
@@ -1336,6 +1355,24 @@ 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
|
||||
our [installing on k8s](https://superset.apache.org/docs/installation/running-on-kubernetes)
|
||||
with our [installing on k8s](https://superset.apache.org/docs/installation/running-on-kubernetes)
|
||||
documentation.
|
||||
:::
|
||||
|
||||
@@ -43,7 +43,6 @@ 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,6 +150,9 @@ 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.
|
||||
|
||||
:::
|
||||
|
||||
@@ -157,9 +160,9 @@ The following example installs the drivers for BigQuery and Elasticsearch, allow
|
||||
```yaml
|
||||
bootstrapScript: |
|
||||
#!/bin/bash
|
||||
pip install psycopg2==2.9.6 \
|
||||
sqlalchemy-bigquery==1.6.1 \
|
||||
elasticsearch-dbapi==0.2.5 &&\
|
||||
uv pip install .[postgres] \
|
||||
.[bigquery] \
|
||||
.[elasticsearch] &&\
|
||||
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.1",
|
||||
"less": "^4.2.2",
|
||||
"less-loader": "^11.0.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -137,4 +137,9 @@ 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
Normal file
BIN
docs/static/img/databases/tdengine.png
vendored
Normal file
Binary file not shown.
|
After 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.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.2.1.tgz#fe4c9848525ab44614c0cf2c00abd8d031bb619a"
|
||||
integrity sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==
|
||||
less@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.2.2.tgz#4b59ede113933b58ab152190edf9180fc36846d8"
|
||||
integrity sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==
|
||||
dependencies:
|
||||
copy-anything "^2.0.1"
|
||||
parse-node-version "^1.0.1"
|
||||
|
||||
@@ -87,7 +87,6 @@ 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",
|
||||
@@ -156,6 +155,7 @@ 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,6 +172,10 @@ 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,8 +329,6 @@ 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,10 +738,6 @@ 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
superset-embedded-sdk/.nvmrc
Normal file
1
superset-embedded-sdk/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v20.16.0
|
||||
@@ -74,7 +74,7 @@ module.exports = {
|
||||
'file-progress',
|
||||
'lodash',
|
||||
'theme-colors',
|
||||
'translation-vars',
|
||||
'i18n-strings',
|
||||
'react-prefer-function-component',
|
||||
'prettier',
|
||||
],
|
||||
@@ -284,7 +284,7 @@ module.exports = {
|
||||
],
|
||||
rules: {
|
||||
'theme-colors/no-literal-colors': 0,
|
||||
'translation-vars/no-template-vars': 0,
|
||||
'i18n-strings/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',
|
||||
'translation-vars/no-template-vars': ['error', true],
|
||||
'i18n-strings/no-template-vars': ['error', true],
|
||||
camelcase: [
|
||||
'error',
|
||||
{
|
||||
@@ -354,6 +354,14 @@ 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',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
);
|
||||
waitForChartLoad(mapSpec);
|
||||
cy.get('[data-test="refresh-chart-menu-item"]').should(
|
||||
'not.have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
'antd5-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',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
'antd5-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',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
'antd5-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',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,15 +54,14 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
|
||||
interceptV1ChartData();
|
||||
}
|
||||
|
||||
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
|
||||
.first()
|
||||
cy.get('.antd5-dropdown:not(.antd5-dropdown-hidden)')
|
||||
.should('be.visible')
|
||||
.find("[role='menu'] [role='menuitem']")
|
||||
.contains(/^Drill by$/)
|
||||
.trigger('mouseover', { force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
'.antd5-dropdown-menu-submenu:not(.antd5-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
|
||||
@@ -61,15 +61,14 @@ function drillToDetail(targetMenuItem: string) {
|
||||
const drillToDetailBy = (targetDrill: string) => {
|
||||
interceptSamples();
|
||||
|
||||
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
|
||||
.first()
|
||||
cy.get('.antd5-dropdown:not(.antd5-dropdown-hidden)')
|
||||
.should('be.visible')
|
||||
.find("[role='menu'] [role='menuitem']")
|
||||
.contains(/^Drill to detail by$/)
|
||||
.trigger('mouseover', { force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-to-detail-by-submenu"]',
|
||||
'.antd5-dropdown-menu-submenu:not(.antd5-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-menu-item-selected')
|
||||
cy.get('.antd5-dropdown-menu-item-selected')
|
||||
.contains('Horizontal (Top)')
|
||||
.should('exist');
|
||||
cy.get('.antd5-menu-item').contains('Vertical (Left)').click();
|
||||
cy.get('.antd5-dropdown-menu-item').contains('Vertical (Left)').click();
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
} else {
|
||||
cy.get('.antd5-menu-item-selected')
|
||||
cy.get('.antd5-dropdown-menu-item-selected')
|
||||
.contains('Vertical (Left)')
|
||||
.should('exist');
|
||||
cy.get('.antd5-menu-item').contains('Horizontal (Top)').click();
|
||||
cy.get('.antd5-dropdown-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('.ant-popover-inner-content').scrollTo('bottom');
|
||||
cy.get('.antd5-popover-inner').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.get('.filterStatusPopover').contains('test_9').click();
|
||||
cy.getBySel('filter-status-popover').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('.ant-popover-content .ant-select-selector')
|
||||
cy.get('.antd5-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('.ant-popover-inner-content')
|
||||
cy.get('.antd5-popover-inner-content')
|
||||
.find('[class^=ant-input]')
|
||||
.first()
|
||||
.type(`${startRange}`);
|
||||
}
|
||||
if (endRange) {
|
||||
cy.get('.ant-popover-inner-content')
|
||||
cy.get('.antd5-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('.ant-dropdown-menu-submenu-title')
|
||||
cy.get('.antd5-dropdown-menu-submenu-title')
|
||||
.contains('On dashboards')
|
||||
.trigger('mouseover', { force: true });
|
||||
}
|
||||
|
||||
function closeDashboardsAddedTo() {
|
||||
cy.get('.ant-dropdown-menu-submenu-title')
|
||||
cy.get('.antd5-dropdown-menu-submenu-title')
|
||||
.contains('On dashboards')
|
||||
.trigger('mouseout', { force: true });
|
||||
cy.getBySel('actions-trigger').click();
|
||||
}
|
||||
|
||||
function verifyDashboardsSubmenuItem(dashboardName) {
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').contains(dashboardName);
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').contains(dashboardName);
|
||||
closeDashboardsAddedTo();
|
||||
}
|
||||
|
||||
function verifyDashboardSearch() {
|
||||
openDashboardsAddedTo();
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup')
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup')
|
||||
.find('input[placeholder="Search"]')
|
||||
.type('1');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').contains('1 - Sample dashboard');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup')
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').contains('1 - Sample dashboard');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup')
|
||||
.find('input[placeholder="Search"]')
|
||||
.type('Blahblah');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').contains('No results found');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup')
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').contains('No results found');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup')
|
||||
.find('[aria-label="close-circle"]')
|
||||
.click();
|
||||
closeDashboardsAddedTo();
|
||||
@@ -68,8 +68,8 @@ function verifyDashboardSearch() {
|
||||
function verifyDashboardLink() {
|
||||
interceptDashboardGet();
|
||||
openDashboardsAddedTo();
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup a')
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup a')
|
||||
.first()
|
||||
.invoke('removeAttr', 'target')
|
||||
.click();
|
||||
|
||||
@@ -51,8 +51,8 @@ describe('Datasource control', () => {
|
||||
)
|
||||
.first()
|
||||
.focus();
|
||||
cy.focused().clear();
|
||||
cy.focused().type(`${newMetricName}{enter}`);
|
||||
cy.focused().clear({ force: true });
|
||||
cy.focused().type(`${newMetricName}{enter}`, { force: true });
|
||||
|
||||
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 .ant-dropdown-trigger').click();
|
||||
cy.get(':nth-child(3) > .ant-dropdown-menu-submenu-title').click();
|
||||
cy.get('.header-with-actions .antd5-dropdown-trigger').click();
|
||||
cy.get(':nth-child(3) > .antd5-dropdown-menu-submenu-title').click();
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)',
|
||||
'.antd5-dropdown-menu-submenu > .antd5-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('.ant-dropdown-menu')
|
||||
cy.get('.antd5-dropdown-menu')
|
||||
.last()
|
||||
.find('.ant-dropdown-menu-item')
|
||||
.find('.antd5-dropdown-menu-item')
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
||||
|
||||
@@ -158,10 +158,10 @@ export const sqlLabView = {
|
||||
runButton: '.css-d3dxop',
|
||||
},
|
||||
rowsLimit: {
|
||||
dropdown: '.ant-dropdown-menu',
|
||||
limitButton: '.ant-dropdown-menu-item',
|
||||
dropdown: '.antd5-dropdown-menu',
|
||||
limitButton: '.antd5-dropdown-menu-item',
|
||||
limitButtonText: '.css-151uxnz',
|
||||
limitTextWithValue: '[class="ant-dropdown-trigger"]',
|
||||
limitTextWithValue: '[class="antd5-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: '.ant-popover-content',
|
||||
container: '.antd5-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 .ant-dropdown-trigger',
|
||||
'.header-with-actions .right-button-panel .antd5-dropdown-trigger',
|
||||
threeDotsMenuDropdown: dataTestLocator('header-actions-menu'),
|
||||
refreshDashboard: dataTestLocator('refresh-dashboard-menu-item'),
|
||||
saveAsMenuOption: dataTestLocator('save-as-menu-item'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "eslint-plugin-translation-vars",
|
||||
"name": "eslint-plugin-i18n-strings",
|
||||
"version": "1.0.0",
|
||||
"description": "Warns about translation variables",
|
||||
"main": "index.js",
|
||||
@@ -75,4 +75,5 @@ 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=4096\" jest --watch",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --max-workers=50%",
|
||||
"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",
|
||||
"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,6 +139,7 @@
|
||||
"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",
|
||||
@@ -253,7 +254,7 @@
|
||||
"@storybook/react-webpack5": "8.1.11",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
@@ -301,6 +302,7 @@
|
||||
"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",
|
||||
@@ -321,8 +323,7 @@
|
||||
"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:tools/eslint-plugin-theme-colors",
|
||||
"eslint-plugin-translation-vars": "file:tools/eslint-plugin-translation-vars",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"exports-loader": "^5.0.0",
|
||||
"fetch-mock": "^7.7.3",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
@@ -331,9 +332,7 @@
|
||||
"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';
|
||||
import { Popover } from 'antd-v5';
|
||||
import type ReactAce from 'react-ace';
|
||||
import type { PopoverProps } from 'antd/lib/popover';
|
||||
import type { PopoverProps } from 'antd-v5/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"
|
||||
arrowPointAtCenter
|
||||
arrow={{ pointAtCenter: true }}
|
||||
title={t('SQL expression')}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -84,6 +84,12 @@ 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 {
|
||||
@@ -515,6 +521,13 @@ 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,17 +19,23 @@
|
||||
|
||||
import { QueryFormMetric, isSavedMetric, isAdhocMetricSimple } from './types';
|
||||
|
||||
export default function getMetricLabel(metric: QueryFormMetric): string {
|
||||
export default function getMetricLabel(
|
||||
metric: QueryFormMetric,
|
||||
index?: number,
|
||||
queryFormMetrics?: QueryFormMetric[],
|
||||
verboseMap?: Record<string, string>,
|
||||
): string {
|
||||
let label = '';
|
||||
if (isSavedMetric(metric)) {
|
||||
return metric;
|
||||
}
|
||||
if (metric.label) {
|
||||
return metric.label;
|
||||
}
|
||||
if (isAdhocMetricSimple(metric)) {
|
||||
return `${metric.aggregate}(${
|
||||
label = metric;
|
||||
} else if (metric.label) {
|
||||
({ label } = metric);
|
||||
} else if (isAdhocMetricSimple(metric)) {
|
||||
label = `${metric.aggregate}(${
|
||||
metric.column.columnName || metric.column.column_name
|
||||
})`;
|
||||
} else {
|
||||
label = metric.sqlExpression;
|
||||
}
|
||||
return metric.sqlExpression;
|
||||
return verboseMap?.[label] || label;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Maybe, QueryFormMetric } from '../../types';
|
||||
import { Currency, 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<string>;
|
||||
currency?: Maybe<Currency>;
|
||||
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 wrapper = shallow(
|
||||
const { container } = render(
|
||||
<WithLegend
|
||||
className="test-class"
|
||||
renderChart={renderChart}
|
||||
renderLegend={renderLegend}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.hasClass('test-class')).toEqual(true);
|
||||
expect(container.querySelectorAll('.test-class')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders when renderLegend is not set', () => {
|
||||
const wrapper = mount(
|
||||
const { container } = render(
|
||||
<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(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(0);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(0);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = mount(
|
||||
const { container } = render(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
width={500}
|
||||
@@ -77,13 +77,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders without width or height', () => {
|
||||
const wrapper = mount(
|
||||
const { container } = render(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
renderChart={renderChart}
|
||||
@@ -96,13 +96,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders legend on the left', () => {
|
||||
const wrapper = mount(
|
||||
const { container } = render(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
position="left"
|
||||
@@ -116,13 +116,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders legend on the right', () => {
|
||||
const wrapper = mount(
|
||||
const { container } = render(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
position="right"
|
||||
@@ -136,13 +136,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders legend on the top', () => {
|
||||
const wrapper = mount(
|
||||
const { container } = render(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
position="top"
|
||||
@@ -156,13 +156,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders legend on the bottom', () => {
|
||||
const wrapper = mount(
|
||||
const { container } = render(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
position="bottom"
|
||||
@@ -176,13 +176,13 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('renders legend with justifyContent set', () => {
|
||||
const wrapper = mount(
|
||||
const { container } = render(
|
||||
<WithLegend
|
||||
debounceTime={1}
|
||||
position="right"
|
||||
@@ -197,8 +197,8 @@ describe.skip('WithLegend', () => {
|
||||
return promiseTimeout(() => {
|
||||
expect(renderChart).toHaveBeenCalledTimes(1);
|
||||
expect(renderLegend).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.render().find('div.chart')).toHaveLength(1);
|
||||
expect(wrapper.render().find('div.legend')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.chart')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('div.legend')).toHaveLength(1);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,16 +16,15 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import ChartClient from '../../../src/chart/clients/ChartClient';
|
||||
import ChartDataProvider, {
|
||||
ChartDataProviderProps,
|
||||
} from '../../../src/chart/components/ChartDataProvider';
|
||||
import { bigNumberFormData } from '../fixtures/formData';
|
||||
|
||||
// Note: the mock implementation of these function directly affects the expected results below
|
||||
// Keep existing mock setup
|
||||
const defaultMockLoadFormData = jest.fn(({ formData }: { formData: unknown }) =>
|
||||
Promise.resolve(formData),
|
||||
);
|
||||
@@ -50,7 +49,6 @@ 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,
|
||||
@@ -62,7 +60,6 @@ const ChartClientMock = ChartClient as jest.Mock<ChartClient>;
|
||||
describe('ChartDataProvider', () => {
|
||||
beforeEach(() => {
|
||||
ChartClientMock.mockClear();
|
||||
|
||||
mockLoadFormData = defaultMockLoadFormData;
|
||||
mockLoadFormData.mockClear();
|
||||
mockLoadDatasource.mockClear();
|
||||
@@ -71,11 +68,17 @@ describe('ChartDataProvider', () => {
|
||||
|
||||
const props: ChartDataProviderProps = {
|
||||
formData: { ...bigNumberFormData },
|
||||
children: () => <div />,
|
||||
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>
|
||||
),
|
||||
};
|
||||
|
||||
function setup(overrideProps?: Partial<ChartDataProviderProps>) {
|
||||
return shallow(<ChartDataProvider {...props} {...overrideProps} />);
|
||||
return render(<ChartDataProvider {...props} {...overrideProps} />);
|
||||
}
|
||||
|
||||
it('instantiates a new ChartClient()', () => {
|
||||
@@ -86,7 +89,7 @@ describe('ChartDataProvider', () => {
|
||||
describe('ChartClient.loadFormData', () => {
|
||||
it('calls method on mount', () => {
|
||||
setup();
|
||||
expect(mockLoadFormData.mock.calls).toHaveLength(1);
|
||||
expect(mockLoadFormData).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadFormData.mock.calls[0][0]).toEqual({
|
||||
sliceId: props.sliceId,
|
||||
formData: props.formData,
|
||||
@@ -96,234 +99,231 @@ describe('ChartDataProvider', () => {
|
||||
it('should pass formDataRequestOptions to ChartClient.loadFormData', () => {
|
||||
const options = { host: 'override' };
|
||||
setup({ formDataRequestOptions: options });
|
||||
expect(mockLoadFormData.mock.calls).toHaveLength(1);
|
||||
expect(mockLoadFormData).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadFormData.mock.calls[0][1]).toEqual(options);
|
||||
});
|
||||
|
||||
it('calls ChartClient.loadFormData when formData or sliceId change', () => {
|
||||
const wrapper = setup();
|
||||
it('calls ChartClient.loadFormData when formData or sliceId change', async () => {
|
||||
const { rerender } = setup();
|
||||
const newProps = { sliceId: 123, formData: undefined };
|
||||
expect(mockLoadFormData.mock.calls).toHaveLength(1);
|
||||
expect(mockLoadFormData).toHaveBeenCalledTimes(1);
|
||||
|
||||
wrapper.setProps(newProps);
|
||||
expect(mockLoadFormData.mock.calls).toHaveLength(2);
|
||||
rerender(<ChartDataProvider {...props} {...newProps} />);
|
||||
expect(mockLoadFormData).toHaveBeenCalledTimes(2);
|
||||
expect(mockLoadFormData.mock.calls[1][0]).toEqual(newProps);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ChartClient.loadDatasource', () => {
|
||||
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('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('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('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('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('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('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,
|
||||
});
|
||||
it('calls ChartClient.loadDatasource if loadDatasource is true and formData or sliceId change', async () => {
|
||||
const { rerender } = setup({ loadDatasource: true });
|
||||
const newDatasource = 'test';
|
||||
|
||||
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);
|
||||
}));
|
||||
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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ChartClient.loadQueryData', () => {
|
||||
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('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('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('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('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 });
|
||||
it('calls ChartClient.loadQueryData when formData or sliceId change', async () => {
|
||||
const { rerender } = setup();
|
||||
const newFormData = { key: 'test' };
|
||||
|
||||
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);
|
||||
}));
|
||||
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]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('children', () => {
|
||||
it('calls children({ loading: true }) when loading', () => {
|
||||
const children = jest.fn<ReactNode, unknown[]>();
|
||||
setup({ children });
|
||||
it('shows loading state initially', async () => {
|
||||
mockLoadFormData.mockImplementation(() => new Promise(() => {}));
|
||||
mockLoadQueryData.mockImplementation(() => new Promise(() => {}));
|
||||
mockLoadDatasource.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
// 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 });
|
||||
setup();
|
||||
await screen.findByRole('status');
|
||||
});
|
||||
|
||||
it('calls children({ payload }) when loaded', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const children = jest.fn<ReactNode, unknown[]>();
|
||||
setup({ children, loadDatasource: true });
|
||||
it('shows payload when loaded', async () => {
|
||||
mockLoadFormData.mockResolvedValue(props.formData);
|
||||
mockLoadQueryData.mockResolvedValue([props.formData]);
|
||||
mockLoadDatasource.mockResolvedValue(props.formData.datasource);
|
||||
|
||||
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);
|
||||
}));
|
||||
setup({ loadDatasource: true });
|
||||
|
||||
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')));
|
||||
const payloadElement = await screen.findByRole('contentinfo');
|
||||
const actualPayload = JSON.parse(payloadElement.textContent || '');
|
||||
|
||||
setup({ children });
|
||||
expect(actualPayload).toEqual({
|
||||
formData: props.formData,
|
||||
datasource: props.formData.datasource,
|
||||
queriesData: [props.formData],
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(children.mock.calls).toHaveLength(2); // loading + error
|
||||
expect(children.mock.calls[1][0]).toEqual({
|
||||
error: new Error('error'),
|
||||
});
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
it('shows error message upon request error', async () => {
|
||||
const errorMessage = 'error';
|
||||
mockLoadFormData.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
it('calls children({ error }) upon JS error', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const children = jest.fn<ReactNode, unknown[]>();
|
||||
setup();
|
||||
|
||||
mockLoadFormData = jest.fn(() => {
|
||||
throw new Error('non-async error');
|
||||
});
|
||||
const errorElement = await screen.findByRole('alert');
|
||||
expect(errorElement).toHaveAttribute('role', 'alert');
|
||||
expect(errorElement).toHaveTextContent(errorMessage);
|
||||
});
|
||||
|
||||
setup({ children });
|
||||
it('shows error message upon JS error', async () => {
|
||||
mockLoadFormData.mockImplementation(() => {
|
||||
throw new Error('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);
|
||||
}));
|
||||
setup();
|
||||
|
||||
const errorElement = await screen.findByRole('alert');
|
||||
expect(errorElement).toHaveAttribute('role', 'alert');
|
||||
expect(errorElement).toHaveTextContent('non-async error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('callbacks', () => {
|
||||
it('calls onLoad(payload) when loaded', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const onLoaded = jest.fn<void, unknown[]>();
|
||||
setup({ onLoaded, loadDatasource: true });
|
||||
it('calls onLoaded when loaded', async () => {
|
||||
const onLoaded = jest.fn();
|
||||
mockLoadFormData.mockResolvedValue(props.formData);
|
||||
mockLoadQueryData.mockResolvedValue([props.formData]);
|
||||
mockLoadDatasource.mockResolvedValue(props.formData.datasource);
|
||||
|
||||
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);
|
||||
}));
|
||||
setup({ onLoaded, loadDatasource: true });
|
||||
|
||||
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')));
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
setup({ onError });
|
||||
setTimeout(() => {
|
||||
expect(onError.mock.calls).toHaveLength(1);
|
||||
expect(onError.mock.calls[0][0]).toEqual(new Error('error'));
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
expect(onLoaded).toHaveBeenCalledTimes(1);
|
||||
expect(onLoaded).toHaveBeenCalledWith({
|
||||
formData: props.formData,
|
||||
datasource: props.formData.datasource,
|
||||
queriesData: [props.formData],
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onError(error) upon JS error', () =>
|
||||
new Promise(done => {
|
||||
expect.assertions(2);
|
||||
const onError = jest.fn<void, unknown[]>();
|
||||
it('calls onError upon request error', async () => {
|
||||
const onError = jest.fn();
|
||||
mockLoadFormData.mockRejectedValue(new Error('error'));
|
||||
|
||||
mockLoadFormData = jest.fn(() => {
|
||||
throw new Error('non-async error');
|
||||
});
|
||||
setup({ onError });
|
||||
|
||||
setup({ onError });
|
||||
setTimeout(() => {
|
||||
expect(onError.mock.calls).toHaveLength(1);
|
||||
expect(onError.mock.calls[0][0]).toEqual(
|
||||
new Error('non-async error'),
|
||||
);
|
||||
done(undefined);
|
||||
}, 0);
|
||||
}));
|
||||
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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
* 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,
|
||||
@@ -28,9 +30,7 @@ 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,45 +44,39 @@ const DEFAULT_QUERIES_DATA = [
|
||||
{ data: ['foo2', 'bar2'] },
|
||||
];
|
||||
|
||||
function expectDimension(
|
||||
renderedWrapper: cheerio.Cheerio,
|
||||
width: number,
|
||||
height: number,
|
||||
) {
|
||||
expect(renderedWrapper.find('.dimension').text()).toEqual(
|
||||
[width, height].join('x'),
|
||||
);
|
||||
// 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 || '';
|
||||
}
|
||||
|
||||
const mount = (component: ReactElement) =>
|
||||
enzymeMount(component, {
|
||||
wrappingComponent: ThemeProvider,
|
||||
wrappingComponentProps: { theme: supersetTheme },
|
||||
const renderWithTheme = (component: ReactElement) =>
|
||||
render(component, {
|
||||
wrapper: ({ children }) => (
|
||||
<ThemeProvider theme={supersetTheme}>{children}</ThemeProvider>
|
||||
),
|
||||
});
|
||||
|
||||
// TODO: rewrite to rtl
|
||||
describe.skip('SuperChart', () => {
|
||||
describe('SuperChart', () => {
|
||||
jest.setTimeout(5000);
|
||||
|
||||
let restoreConsole: RestoreConsole;
|
||||
|
||||
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(() => {
|
||||
@@ -105,14 +99,16 @@ describe.skip('SuperChart', () => {
|
||||
|
||||
afterEach(() => {
|
||||
window.removeEventListener('error', onError);
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
});
|
||||
|
||||
it('should have correct number of errors', () => {
|
||||
expect(actualErrors).toBe(expectedErrors);
|
||||
expectedErrors = 0;
|
||||
});
|
||||
|
||||
it('renders default FallbackComponent', async () => {
|
||||
expectedErrors = 1;
|
||||
const wrapper = mount(
|
||||
renderWithTheme(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.BUGGY}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
@@ -120,16 +116,19 @@ describe.skip('SuperChart', () => {
|
||||
height="200"
|
||||
/>,
|
||||
);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
wrapper.update();
|
||||
expect(wrapper.text()).toContain('Oops! An error occurred!');
|
||||
|
||||
expect(
|
||||
await screen.findByText('Oops! An error occurred!'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it('renders custom FallbackComponent', () => {
|
||||
|
||||
it('renders custom FallbackComponent', async () => {
|
||||
expectedErrors = 1;
|
||||
const CustomFallbackComponent = jest.fn(() => (
|
||||
<div>Custom Fallback!</div>
|
||||
));
|
||||
const wrapper = mount(
|
||||
|
||||
renderWithTheme(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.BUGGY}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
@@ -139,15 +138,13 @@ describe.skip('SuperChart', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(0);
|
||||
expect(CustomFallbackComponent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(await screen.findByText('Custom Fallback!')).toBeInTheDocument();
|
||||
expect(CustomFallbackComponent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('call onErrorBoundary', () => {
|
||||
it('call onErrorBoundary', async () => {
|
||||
expectedErrors = 1;
|
||||
const handleError = jest.fn();
|
||||
mount(
|
||||
renderWithTheme(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.BUGGY}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
@@ -157,17 +154,20 @@ describe.skip('SuperChart', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(handleError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
await screen.findByText('Oops! An error occurred!');
|
||||
expect(handleError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('does not include ErrorBoundary if told so', () => {
|
||||
|
||||
// Update the test cases
|
||||
it('does not include ErrorBoundary if told so', async () => {
|
||||
expectedErrors = 1;
|
||||
const inactiveErrorHandler = jest.fn();
|
||||
const activeErrorHandler = jest.fn();
|
||||
mount(
|
||||
// @ts-ignore
|
||||
<ErrorBoundary onError={activeErrorHandler}>
|
||||
renderWithTheme(
|
||||
<ErrorBoundary
|
||||
fallbackRender={() => <div>Error!</div>}
|
||||
onError={activeErrorHandler}
|
||||
>
|
||||
<SuperChart
|
||||
disableErrorBoundary
|
||||
chartType={ChartKeys.BUGGY}
|
||||
@@ -179,15 +179,24 @@ describe.skip('SuperChart', () => {
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(activeErrorHandler).toHaveBeenCalledTimes(1);
|
||||
expect(inactiveErrorHandler).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
await screen.findByText('Error!');
|
||||
expect(activeErrorHandler).toHaveBeenCalledTimes(1);
|
||||
expect(inactiveErrorHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the props to renderer correctly', () => {
|
||||
const wrapper = mount(
|
||||
// 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(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
@@ -197,15 +206,123 @@ describe.skip('SuperChart', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
const renderedWrapper = wrapper.render();
|
||||
expect(renderedWrapper.find('div.test-component')).toHaveLength(1);
|
||||
expectDimension(renderedWrapper, 101, 118);
|
||||
await promiseTimeout(() => {
|
||||
const testComponent = findByClassName(container, 'test-component');
|
||||
expect(testComponent).not.toBeNull();
|
||||
expect(testComponent).toBeInTheDocument();
|
||||
expect(getDimensionText(container)).toBe('101x118');
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the props with multiple queries to renderer correctly', () => {
|
||||
const wrapper = mount(
|
||||
// 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(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={DEFAULT_QUERIES_DATA}
|
||||
@@ -215,42 +332,25 @@ describe.skip('SuperChart', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
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);
|
||||
await promiseTimeout(() => {
|
||||
const testComponent = container.querySelector('.test-component');
|
||||
expect(testComponent).not.toBeNull();
|
||||
expect(testComponent).toBeInTheDocument();
|
||||
expect(getDimensionText(container)).toBe('101x118');
|
||||
});
|
||||
});
|
||||
|
||||
describe('supports NoResultsComponent', () => {
|
||||
it('renders NoResultsComponent when queriesData is missing', () => {
|
||||
const wrapper = mount(
|
||||
renderWithTheme(
|
||||
<SuperChart chartType={ChartKeys.DILIGENT} width="200" height="200" />,
|
||||
);
|
||||
|
||||
expect(wrapper.find(NoResultsComponent)).toHaveLength(1);
|
||||
expect(screen.getByText('No Results')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders NoResultsComponent when queriesData data is null', () => {
|
||||
const wrapper = mount(
|
||||
renderWithTheme(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[{ data: null }]}
|
||||
@@ -259,116 +359,12 @@ describe.skip('SuperChart', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(NoResultsComponent)).toHaveLength(1);
|
||||
expect(screen.getByText('No Results')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('supports dynamic width and/or height', () => {
|
||||
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', () => {
|
||||
// Add MyWrapper component definition
|
||||
function MyWrapper({ width, height, children }: WrapperProps) {
|
||||
return (
|
||||
<div>
|
||||
@@ -380,50 +376,81 @@ describe.skip('SuperChart', () => {
|
||||
);
|
||||
}
|
||||
|
||||
it('works with width and height that are numbers', () => {
|
||||
const wrapper = mount(
|
||||
it('works with width and height that are numbers', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuperChart
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
queriesData={[DEFAULT_QUERY_DATA]}
|
||||
width={100}
|
||||
height={100}
|
||||
Wrapper={MyWrapper}
|
||||
/>,
|
||||
);
|
||||
|
||||
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);
|
||||
await promiseTimeout(() => {
|
||||
const testComponent = container.querySelector('.test-component');
|
||||
expect(testComponent).not.toBeNull();
|
||||
expect(testComponent).toBeInTheDocument();
|
||||
expect(getDimensionText(container)).toBe('100x100');
|
||||
});
|
||||
});
|
||||
|
||||
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}
|
||||
/>,
|
||||
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>,
|
||||
);
|
||||
triggerResizeObserver();
|
||||
|
||||
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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,16 +17,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import '@testing-library/jest-dom';
|
||||
import { ReactElement } from 'react';
|
||||
import mockConsole, { RestoreConsole } from 'jest-mock-console';
|
||||
import {
|
||||
ChartProps,
|
||||
promiseTimeout,
|
||||
supersetTheme,
|
||||
SupersetTheme,
|
||||
ThemeProvider,
|
||||
} from '@superset-ui/core';
|
||||
import { ChartProps, supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import SuperChartCore from '../../../src/chart/components/SuperChartCore';
|
||||
import {
|
||||
ChartKeys,
|
||||
@@ -35,25 +30,11 @@ import {
|
||||
SlowChartPlugin,
|
||||
} from './MockChartPlugins';
|
||||
|
||||
const Wrapper = ({
|
||||
theme,
|
||||
children,
|
||||
}: {
|
||||
theme: SupersetTheme;
|
||||
children: ReactNode;
|
||||
}) => <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||
|
||||
const styledMount = (component: ReactElement) =>
|
||||
mount(component, {
|
||||
wrappingComponent: Wrapper,
|
||||
wrappingComponentProps: {
|
||||
theme: supersetTheme,
|
||||
},
|
||||
});
|
||||
const renderWithTheme = (component: ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
describe('SuperChartCore', () => {
|
||||
const chartProps = new ChartProps();
|
||||
|
||||
const plugins = [
|
||||
new DiligentChartPlugin().configure({ key: ChartKeys.DILIGENT }),
|
||||
new LazyChartPlugin().configure({ key: ChartKeys.LAZY }),
|
||||
@@ -63,6 +44,7 @@ describe('SuperChartCore', () => {
|
||||
let restoreConsole: RestoreConsole;
|
||||
|
||||
beforeAll(() => {
|
||||
jest.setTimeout(30000);
|
||||
plugins.forEach(p => {
|
||||
p.unregister().register();
|
||||
});
|
||||
@@ -83,72 +65,83 @@ describe('SuperChartCore', () => {
|
||||
});
|
||||
|
||||
describe('registered charts', () => {
|
||||
it('renders registered chart', () => {
|
||||
const wrapper = styledMount(
|
||||
it('renders registered chart', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuperChartCore
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
chartProps={chartProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(1);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.test-component')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('renders registered chart with lazy loading', () => {
|
||||
const wrapper = styledMount(
|
||||
|
||||
it('renders registered chart with lazy loading', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuperChartCore chartType={ChartKeys.LAZY} />,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(1);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.test-component')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('does not render if chartType is not set', () => {
|
||||
// Suppress warning
|
||||
// @ts-ignore chartType is required
|
||||
const wrapper = styledMount(<SuperChartCore />);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().children()).toHaveLength(0);
|
||||
}, 5);
|
||||
it('does not render if chartType is not set', async () => {
|
||||
// @ts-ignore chartType is required
|
||||
const { container } = renderWithTheme(<SuperChartCore />);
|
||||
|
||||
await waitFor(() => {
|
||||
const testComponent = container.querySelector('.test-component');
|
||||
expect(testComponent).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('adds id to container if specified', () => {
|
||||
const wrapper = styledMount(
|
||||
|
||||
it('adds id to container if specified', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuperChartCore chartType={ChartKeys.DILIGENT} id="the-chart" />,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().attr('id')).toEqual('the-chart');
|
||||
await waitFor(() => {
|
||||
const element = container.querySelector('#the-chart');
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element).toHaveAttribute('id', 'the-chart');
|
||||
});
|
||||
});
|
||||
it('adds class to container if specified', () => {
|
||||
const wrapper = styledMount(
|
||||
|
||||
it('adds class to container if specified', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuperChartCore chartType={ChartKeys.DILIGENT} className="the-chart" />,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.hasClass('the-chart')).toBeTruthy();
|
||||
}, 0);
|
||||
await waitFor(() => {
|
||||
const element = container.querySelector('.the-chart');
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element).toHaveClass('the-chart');
|
||||
});
|
||||
});
|
||||
it('uses overrideTransformProps when specified', () => {
|
||||
const wrapper = styledMount(
|
||||
|
||||
it('uses overrideTransformProps when specified', async () => {
|
||||
renderWithTheme(
|
||||
<SuperChartCore
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
overrideTransformProps={() => ({ message: 'hulk' })}
|
||||
/>,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('.message').text()).toEqual('hulk');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('hulk')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('uses preTransformProps when specified', () => {
|
||||
|
||||
it('uses preTransformProps when specified', async () => {
|
||||
const chartPropsWithPayload = new ChartProps({
|
||||
queriesData: [{ message: 'hulk' }],
|
||||
theme: supersetTheme,
|
||||
});
|
||||
const wrapper = styledMount(
|
||||
|
||||
renderWithTheme(
|
||||
<SuperChartCore
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
preTransformProps={() => chartPropsWithPayload}
|
||||
@@ -156,69 +149,77 @@ describe('SuperChartCore', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('.message').text()).toEqual('hulk');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('hulk')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('uses postTransformProps when specified', () => {
|
||||
const wrapper = styledMount(
|
||||
|
||||
it('uses postTransformProps when specified', async () => {
|
||||
renderWithTheme(
|
||||
<SuperChartCore
|
||||
chartType={ChartKeys.DILIGENT}
|
||||
postTransformProps={() => ({ message: 'hulk' })}
|
||||
/>,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('.message').text()).toEqual('hulk');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('hulk')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('renders if chartProps is not specified', () => {
|
||||
const wrapper = styledMount(
|
||||
|
||||
it('renders if chartProps is not specified', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuperChartCore chartType={ChartKeys.DILIGENT} />,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(1);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.test-component')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render anything while waiting for Chart code to load', () => {
|
||||
const wrapper = styledMount(
|
||||
const { container } = renderWithTheme(
|
||||
<SuperChartCore chartType={ChartKeys.SLOW} />,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().children()).toHaveLength(0);
|
||||
});
|
||||
const testComponent = container.querySelector('.test-component');
|
||||
expect(testComponent).not.toBeInTheDocument();
|
||||
});
|
||||
it('eventually renders after Chart is loaded', () => {
|
||||
// Suppress warning
|
||||
const wrapper = styledMount(
|
||||
|
||||
it('eventually renders after Chart is loaded', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuperChartCore chartType={ChartKeys.SLOW} />,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(1);
|
||||
}, 1500);
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(
|
||||
container.querySelector('.test-component'),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
it('does not render if chartProps is null', () => {
|
||||
const wrapper = styledMount(
|
||||
|
||||
it('does not render if chartProps is null', async () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuperChartCore chartType={ChartKeys.DILIGENT} chartProps={null} />,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('div.test-component')).toHaveLength(0);
|
||||
await waitFor(() => {
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unregistered charts', () => {
|
||||
it('renders error message', () => {
|
||||
const wrapper = styledMount(
|
||||
it('renders error message', async () => {
|
||||
renderWithTheme(
|
||||
<SuperChartCore chartType="4d-pie-chart" chartProps={chartProps} />,
|
||||
);
|
||||
|
||||
return promiseTimeout(() => {
|
||||
expect(wrapper.render().find('.alert')).toHaveLength(1);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
* 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)', () => {
|
||||
@@ -78,14 +79,18 @@ describe('reactify(renderFn)', () => {
|
||||
|
||||
it('returns a React component class', () =>
|
||||
new Promise(done => {
|
||||
const wrapper = mount(<TestComponent />);
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(renderFn).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.html()).toEqual('<div id="test"><b>abc</b></div>');
|
||||
expect(screen.getByText('abc')).toBeInTheDocument();
|
||||
expect(screen.getByText('abc').parentNode).toHaveAttribute('id', 'test');
|
||||
setTimeout(() => {
|
||||
expect(renderFn).toHaveBeenCalledTimes(2);
|
||||
expect(wrapper.html()).toEqual('<div id="test"><b>def</b></div>');
|
||||
wrapper.unmount();
|
||||
expect(screen.getByText('def')).toBeInTheDocument();
|
||||
expect(screen.getByText('def').parentNode).toHaveAttribute(
|
||||
'id',
|
||||
'test',
|
||||
);
|
||||
done(undefined);
|
||||
}, 20);
|
||||
}));
|
||||
@@ -119,8 +124,9 @@ describe('reactify(renderFn)', () => {
|
||||
describe('defaultProps', () => {
|
||||
it('has defaultProps if renderFn.defaultProps is defined', () => {
|
||||
expect(TheChart.defaultProps).toBe(renderFn.defaultProps);
|
||||
const wrapper = mount(<TheChart id="test" />);
|
||||
expect(wrapper.html()).toEqual('<div id="test"><b>ghi</b></div>');
|
||||
render(<TheChart id="test" />);
|
||||
expect(screen.getByText('ghi')).toBeInTheDocument();
|
||||
expect(screen.getByText('ghi').parentNode).toHaveAttribute('id', 'test');
|
||||
});
|
||||
it('does not have defaultProps if renderFn.defaultProps is not defined', () => {
|
||||
const AnotherChart = reactify(() => {});
|
||||
@@ -136,9 +142,9 @@ describe('reactify(renderFn)', () => {
|
||||
});
|
||||
it('calls willUnmount hook when it is provided', () =>
|
||||
new Promise(done => {
|
||||
const wrapper = mount(<AnotherTestComponent />);
|
||||
const { unmount } = render(<AnotherTestComponent />);
|
||||
setTimeout(() => {
|
||||
wrapper.unmount();
|
||||
unmount();
|
||||
expect(willUnmountCb).toHaveBeenCalledTimes(1);
|
||||
done(undefined);
|
||||
}, 20);
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ensureIsArray,
|
||||
GenericDataType,
|
||||
getCustomFormatter,
|
||||
getMetricLabel,
|
||||
getNumberFormatter,
|
||||
getXAxisLabel,
|
||||
isDefined,
|
||||
@@ -291,12 +292,20 @@ 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,
|
||||
xAxisCol: xAxisLabel,
|
||||
metricsLabels,
|
||||
},
|
||||
);
|
||||
const {
|
||||
@@ -305,7 +314,7 @@ export default function transformProps(
|
||||
} = extractDataTotalValues(rebasedDataB, {
|
||||
stack: Boolean(stackB),
|
||||
percentageThreshold,
|
||||
xAxisCol: xAxisLabel,
|
||||
metricsLabels: metricsLabelsB,
|
||||
});
|
||||
|
||||
annotationLayers
|
||||
|
||||
@@ -215,14 +215,18 @@ 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(
|
||||
@@ -296,7 +300,6 @@ export default function transformProps(
|
||||
const entryName = String(entry.name || '');
|
||||
const seriesName = inverted[entryName] || entryName;
|
||||
const colorScaleKey = getOriginalSeries(seriesName, array);
|
||||
|
||||
const transformedSeries = transformSeries(
|
||||
entry,
|
||||
colorScale,
|
||||
@@ -622,6 +625,7 @@ 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: true,
|
||||
default: false,
|
||||
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 } from '@superset-ui/chart-controls';
|
||||
import { SortSeriesType, LegendPaddingType } 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, xAxisCol, legendState } = opts;
|
||||
const { stack, percentageThreshold, legendState, metricsLabels } = opts;
|
||||
if (stack) {
|
||||
data.forEach(datum => {
|
||||
const values = Object.keys(datum).reduce((prev, curr) => {
|
||||
if (curr === xAxisCol) {
|
||||
if (!metricsLabels.includes(curr)) {
|
||||
return prev;
|
||||
}
|
||||
if (legendState && !legendState[curr]) {
|
||||
@@ -425,6 +425,7 @@ export function getLegendProps(
|
||||
theme: SupersetTheme,
|
||||
zoomable = false,
|
||||
legendState?: LegendState,
|
||||
padding?: LegendPaddingType,
|
||||
): LegendComponentOption | LegendComponentOption[] {
|
||||
const legend: LegendComponentOption | LegendComponentOption[] = {
|
||||
orient: [LegendOrientation.Top, LegendOrientation.Bottom].includes(
|
||||
@@ -443,13 +444,30 @@ 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;
|
||||
@@ -467,7 +485,7 @@ export function getChartPadding(
|
||||
show: boolean,
|
||||
orientation: LegendOrientation,
|
||||
margin?: string | number | null,
|
||||
padding?: { top?: number; bottom?: number; left?: number; right?: number },
|
||||
padding?: LegendPaddingType,
|
||||
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,15 +36,25 @@ describe('EchartsTimeseries transformProps', () => {
|
||||
colorScheme: 'bnbColors',
|
||||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
metric: 'sum__num',
|
||||
metrics: ['sum__num'],
|
||||
groupby: ['foo', 'bar'],
|
||||
viz_type: 'my_viz',
|
||||
};
|
||||
const queriesData = [
|
||||
{
|
||||
data: [
|
||||
{ 'San Francisco': 1, 'New York': 2, __timestamp: 599616000000 },
|
||||
{ 'San Francisco': 3, 'New York': 4, __timestamp: 599916000000 },
|
||||
{
|
||||
'San Francisco': 1,
|
||||
'New York': 2,
|
||||
__timestamp: 599616000000,
|
||||
sum__num: 4,
|
||||
},
|
||||
{
|
||||
'San Francisco': 3,
|
||||
'New York': 4,
|
||||
__timestamp: 599916000000,
|
||||
sum__num: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -64,7 +74,7 @@ describe('EchartsTimeseries transformProps', () => {
|
||||
height: 600,
|
||||
echartOptions: expect.objectContaining({
|
||||
legend: expect.objectContaining({
|
||||
data: ['San Francisco', 'New York'],
|
||||
data: ['sum__num', 'San Francisco', 'New York'],
|
||||
}),
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -101,7 +111,7 @@ describe('EchartsTimeseries transformProps', () => {
|
||||
height: 600,
|
||||
echartOptions: expect.objectContaining({
|
||||
legend: expect.objectContaining({
|
||||
data: ['San Francisco', 'New York'],
|
||||
data: ['sum__num', 'San Francisco', 'New York'],
|
||||
}),
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -146,7 +156,7 @@ describe('EchartsTimeseries transformProps', () => {
|
||||
height: 600,
|
||||
echartOptions: expect.objectContaining({
|
||||
legend: expect.objectContaining({
|
||||
data: ['San Francisco', 'New York', 'My Formula'],
|
||||
data: ['sum__num', 'San Francisco', 'New York', 'My Formula'],
|
||||
}),
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -274,7 +284,7 @@ describe('EchartsTimeseries transformProps', () => {
|
||||
expect.objectContaining({
|
||||
echartOptions: expect.objectContaining({
|
||||
legend: expect.objectContaining({
|
||||
data: ['San Francisco', 'New York', 'My Line'],
|
||||
data: ['sum__num', 'San Francisco', 'New York', 'My Line'],
|
||||
}),
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -420,7 +430,7 @@ describe('Does transformProps transform series correctly', () => {
|
||||
colorScheme: 'bnbColors',
|
||||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
metric: 'sum__num',
|
||||
metrics: ['sum__num'],
|
||||
groupby: ['foo', 'bar'],
|
||||
showValue: true,
|
||||
stack: true,
|
||||
@@ -435,24 +445,28 @@ 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -468,7 +482,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') return stackSum;
|
||||
if (key === '__timestamp' || key === 'sum__num') return stackSum;
|
||||
return stackSum + currentStack[key as keyof typeof currentStack];
|
||||
}, 0);
|
||||
totals.push(total);
|
||||
@@ -561,7 +575,6 @@ 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) => {
|
||||
@@ -576,7 +589,6 @@ describe('Does transformProps transform series correctly', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not apply percentage threshold when showValue is true and stack is false', () => {
|
||||
const updatedChartPropsConfig = {
|
||||
...chartPropsConfig,
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
calculateLowerLogTick,
|
||||
dedupSeries,
|
||||
extractGroupbyLabel,
|
||||
extractDataTotalValues,
|
||||
extractSeries,
|
||||
extractShowValueIndexes,
|
||||
extractTooltipKeys,
|
||||
@@ -1085,6 +1086,123 @@ 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,3 +73,26 @@ 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,5 +99,18 @@ 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,8 +20,9 @@ import {
|
||||
ControlSetItem,
|
||||
CustomControlConfig,
|
||||
sharedControls,
|
||||
InfoTooltipWithTrigger,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t, validateNonEmpty } from '@superset-ui/core';
|
||||
import { t, validateNonEmpty, useTheme, SafeMarkdown } from '@superset-ui/core';
|
||||
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
|
||||
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
|
||||
import { debounceFunc } from '../../consts';
|
||||
@@ -33,13 +34,48 @@ 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>{props.label}</ControlHeader>
|
||||
<ControlHeader>
|
||||
<div>
|
||||
{props.label}
|
||||
<InfoTooltipWithTrigger
|
||||
iconsStyle={{ marginLeft: theme.gridUnit }}
|
||||
tooltip={<SafeMarkdown source={helpersTooltipContent} />}
|
||||
/>
|
||||
</div>
|
||||
</ControlHeader>
|
||||
<CodeEditor
|
||||
theme="dark"
|
||||
value={val}
|
||||
@@ -65,6 +101,7 @@ export const handlebarsTemplateControlSetItem: ControlSetItem = {
|
||||
</ul>`,
|
||||
isInt: false,
|
||||
renderTrigger: true,
|
||||
valueKey: null,
|
||||
|
||||
validators: [validateNonEmpty],
|
||||
mapStateToProps: ({ controls }) => ({
|
||||
|
||||
@@ -75,6 +75,7 @@ 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 'jest-enzyme';
|
||||
import 'enzyme-matchers';
|
||||
import jQuery from 'jquery';
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
||||
|
||||
@@ -109,6 +109,7 @@ 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 { shallow as enzymeShallow, mount as enzymeMount } from 'enzyme';
|
||||
import { mount as enzymeMount } from 'enzyme';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { supersetTheme } from '@superset-ui/core';
|
||||
import { ReactElement } from 'react';
|
||||
@@ -26,12 +26,13 @@ type optionsType = {
|
||||
wrappingComponentProps?: any;
|
||||
wrappingComponent?: ReactElement;
|
||||
context?: any;
|
||||
newOption?: string;
|
||||
};
|
||||
|
||||
export function styledMount(
|
||||
component: ReactElement,
|
||||
options: optionsType = {},
|
||||
) {
|
||||
): any {
|
||||
return enzymeMount(component, {
|
||||
...options,
|
||||
wrappingComponent: ProviderWrapper,
|
||||
@@ -41,17 +42,3 @@ export function styledMount(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function styledShallow(
|
||||
component: ReactElement,
|
||||
options: optionsType = {},
|
||||
) {
|
||||
return enzymeShallow(component, {
|
||||
...options,
|
||||
wrappingComponent: ProviderWrapper,
|
||||
wrappingComponentProps: {
|
||||
theme: supersetTheme,
|
||||
...options?.wrappingComponentProps,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,16 +39,18 @@ 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 {
|
||||
.antd5-picker-dropdown,
|
||||
.ant-popover,
|
||||
.antd5-popover {
|
||||
z-index: ${theme.zIndex.max} !important;
|
||||
}
|
||||
|
||||
@@ -105,13 +107,6 @@ 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 '@testing-library/react';
|
||||
import { waitFor } from 'spec/helpers/testing-library';
|
||||
import * as actions from 'src/SqlLab/actions/sqlLab';
|
||||
import { LOG_EVENT } from 'src/logger/actions';
|
||||
import {
|
||||
|
||||
@@ -20,8 +20,12 @@ import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Store } from 'redux';
|
||||
|
||||
import { render, fireEvent, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
import QueryLimitSelect, {
|
||||
QueryLimitSelectProps,
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { styled, useTheme, t } from '@superset-ui/core';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { useTheme, t } from '@superset-ui/core';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
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;
|
||||
@@ -34,28 +35,6 @@ 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,
|
||||
@@ -94,20 +73,18 @@ const QueryLimitSelect = ({
|
||||
dispatch(queryEditorSetQueryLimit(queryEditor, updatedQueryLimit));
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ 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';
|
||||
|
||||
@@ -29,27 +28,55 @@ const mockedProps = {
|
||||
displayLimit: 100,
|
||||
latestQueryId: 'ryhMUZCGb',
|
||||
};
|
||||
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,
|
||||
|
||||
describe('QueryTable', () => {
|
||||
test('is valid', () => {
|
||||
expect(isValidElement(<QueryTable displayLimit={100} />)).toBe(true);
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<QueryTable {...mockedProps} />
|
||||
</Provider>,
|
||||
);
|
||||
test('is valid with props', () => {
|
||||
expect(isValidElement(<QueryTable {...mockedProps} />)).toBe(true);
|
||||
});
|
||||
|
||||
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);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -143,6 +143,13 @@ 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,
|
||||
@@ -157,8 +164,10 @@ describe('ResultSet', () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
const table = getByTestId('table-container');
|
||||
expect(table).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
const table = getByTestId('table-container');
|
||||
expect(table).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should render success query', async () => {
|
||||
@@ -245,7 +254,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];
|
||||
@@ -508,13 +517,22 @@ describe('ResultSet', () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const downloadButton = getByTestId('export-csv-button');
|
||||
expect(downloadButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const downloadButton = getByTestId('export-csv-button');
|
||||
fireEvent.click(downloadButton);
|
||||
await waitFor(() => fireEvent.click(downloadButton));
|
||||
|
||||
const warningModal = await findByRole('dialog');
|
||||
expect(
|
||||
within(warningModal).getByText(`Download is on the way`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(warningModal).getByText(`Download is on the way`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
}, 20000);
|
||||
|
||||
test('should not allow download as CSV when user does not have permission to export data', async () => {
|
||||
const { queryByTestId } = setup(
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import SaveDatasetActionButton from 'src/SqlLab/components/SaveDatasetActionButton';
|
||||
|
||||
|
||||
@@ -16,12 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t, useTheme, styled } from '@superset-ui/core';
|
||||
import { t, useTheme } 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;
|
||||
@@ -34,34 +32,14 @@ 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>
|
||||
) : (
|
||||
<StyledDropdownButton
|
||||
<DropdownButton
|
||||
onClick={() => setShowSave(true)}
|
||||
overlay={overlayMenu}
|
||||
dropdownRender={() => overlayMenu}
|
||||
icon={
|
||||
<Icons.CaretDown
|
||||
iconColor={theme.colors.grayscale.light5}
|
||||
@@ -71,7 +49,7 @@ const SaveDatasetActionButton = ({
|
||||
trigger={['click']}
|
||||
>
|
||||
{t('Save')}
|
||||
</StyledDropdownButton>
|
||||
</DropdownButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
*/
|
||||
import * as reactRedux from 'react-redux';
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
cleanup,
|
||||
userEvent,
|
||||
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,32 +96,36 @@ interface SaveDatasetModalProps {
|
||||
}
|
||||
|
||||
const Styles = styled.div`
|
||||
${({ theme }) => `
|
||||
.sdm-body {
|
||||
margin: 0 8px;
|
||||
margin: 0 ${theme.gridUnit * 2}px;
|
||||
}
|
||||
.sdm-input {
|
||||
margin-left: 45px;
|
||||
margin-left: ${theme.gridUnit * 10}px;
|
||||
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: 7px;
|
||||
margin: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
.sdm-overwrite-container {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const updateDataset = async (
|
||||
dbId: number,
|
||||
datasetId: number,
|
||||
|
||||
@@ -18,8 +18,12 @@
|
||||
*/
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import SaveQuery from 'src/SqlLab/components/SaveQuery';
|
||||
import { initialState, databases } from 'src/SqlLab/fixtures';
|
||||
|
||||
|
||||
@@ -26,9 +26,13 @@ import {
|
||||
ThemeProvider,
|
||||
isFeatureEnabled,
|
||||
} from '@superset-ui/core';
|
||||
import { render, screen, act, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
act,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import ShareSqlLabQuery from 'src/SqlLab/components/ShareSqlLabQuery';
|
||||
import { initialState } from 'src/SqlLab/fixtures';
|
||||
|
||||
@@ -133,7 +137,7 @@ describe('ShareSqlLabQuery', () => {
|
||||
});
|
||||
});
|
||||
const button = screen.getByRole('button');
|
||||
const { id, remoteId, ...expected } = mockQueryEditor;
|
||||
const { id: _id, remoteId: _remoteId, ...expected } = mockQueryEditor;
|
||||
userEvent.click(button);
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
|
||||
@@ -150,7 +154,7 @@ describe('ShareSqlLabQuery', () => {
|
||||
});
|
||||
});
|
||||
const button = screen.getByRole('button');
|
||||
const { id, ...expected } = unsavedQueryEditor;
|
||||
const { id: _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 } from 'spec/helpers/testing-library';
|
||||
import { render, waitFor, within } 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,12 +49,14 @@ const mockState = {
|
||||
tables: [
|
||||
{
|
||||
...table,
|
||||
id: 't3',
|
||||
name: 'table3',
|
||||
dataPreviewQueryId: '2g2_iRFMl',
|
||||
queryEditorId: defaultQueryEditor.id,
|
||||
},
|
||||
{
|
||||
...table,
|
||||
id: 't4',
|
||||
name: 'table4',
|
||||
dataPreviewQueryId: 'erWdqEWPm',
|
||||
queryEditorId: defaultQueryEditor.id,
|
||||
@@ -149,3 +151,22 @@ 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, queryEditorId],
|
||||
[dispatch, pinnedTables],
|
||||
);
|
||||
|
||||
return offline ? (
|
||||
|
||||
@@ -17,13 +17,18 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { FocusEventHandler } from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
isFeatureEnabled,
|
||||
getExtensionsRegistry,
|
||||
FeatureFlag,
|
||||
} from '@superset-ui/core';
|
||||
import { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
|
||||
import {
|
||||
act,
|
||||
cleanup,
|
||||
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';
|
||||
@@ -135,6 +140,15 @@ 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],
|
||||
@@ -187,16 +201,27 @@ describe('SqlEditor', () => {
|
||||
});
|
||||
|
||||
it('render a SqlEditorLeftBar', async () => {
|
||||
const { getByTestId } = setup(mockedProps, store);
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('mock-sql-editor-left-bar')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
const { getByTestId, unmount } = setup(mockedProps, store);
|
||||
|
||||
await waitFor(
|
||||
() => expect(getByTestId('mock-sql-editor-left-bar')).toBeInTheDocument(),
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
unmount();
|
||||
}, 15000);
|
||||
|
||||
// Update other similar tests with timeouts
|
||||
it('render an AceEditorWrapper', async () => {
|
||||
const { findByTestId } = setup(mockedProps, store);
|
||||
expect(await findByTestId('react-ace')).toBeInTheDocument();
|
||||
});
|
||||
const { findByTestId, unmount } = setup(mockedProps, store);
|
||||
|
||||
await waitFor(
|
||||
() => expect(findByTestId('react-ace')).resolves.toBeInTheDocument(),
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
unmount();
|
||||
}, 15000);
|
||||
|
||||
it('skip rendering an AceEditorWrapper when the current tab is inactive', async () => {
|
||||
const { findByTestId, queryByTestId } = setup(
|
||||
|
||||
@@ -56,7 +56,8 @@ import Mousetrap from 'mousetrap';
|
||||
import Button from 'src/components/Button';
|
||||
import Timer from 'src/components/Timer';
|
||||
import ResizableSidebar from 'src/components/ResizableSidebar';
|
||||
import { AntdDropdown, Skeleton } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Skeleton } from 'src/components';
|
||||
import { Switch } from 'src/components/Switch';
|
||||
import { Input } from 'src/components/Input';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
@@ -868,9 +869,14 @@ const SqlEditor: FC<Props> = ({
|
||||
<span>
|
||||
<ShareSqlLabQuery queryEditorId={queryEditor.id} />
|
||||
</span>
|
||||
<AntdDropdown overlay={renderDropdown()} trigger={['click']}>
|
||||
<Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
|
||||
</AntdDropdown>
|
||||
<Dropdown
|
||||
dropdownRender={() => renderDropdown()}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button buttonSize="xsmall" type="link" showMarginRight={false}>
|
||||
<Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
* under the License.
|
||||
*/
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
within,
|
||||
} from 'spec/helpers/testing-library';
|
||||
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 { Dropdown } from 'src/components/Dropdown';
|
||||
import { MenuDotsDropdown } 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>
|
||||
<Dropdown
|
||||
<MenuDotsDropdown
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
<Menu style={{ width: 176 }}>
|
||||
<Menu>
|
||||
<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,11 +30,8 @@ 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,
|
||||
AntdDropdown,
|
||||
} from 'src/components';
|
||||
import { Skeleton, AntdBreadcrumb as Breadcrumb, Button } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import FilterableTable from 'src/components/FilterableTable';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import {
|
||||
@@ -308,8 +305,8 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
|
||||
<Title>
|
||||
<Icons.Table iconSize="l" />
|
||||
{tableName}
|
||||
<AntdDropdown
|
||||
overlay={
|
||||
<Dropdown
|
||||
dropdownRender={() => (
|
||||
<Menu
|
||||
onClick={({ key }) => {
|
||||
if (key === 'refresh-table') {
|
||||
@@ -324,15 +321,17 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
|
||||
}}
|
||||
items={dropdownMenu}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Icons.DownSquareOutlined
|
||||
iconSize="m"
|
||||
style={{ marginTop: 2, marginLeft: 4 }}
|
||||
aria-label={t('Table actions')}
|
||||
/>
|
||||
</AntdDropdown>
|
||||
<Button buttonSize="xsmall" type="link">
|
||||
<Icons.DownSquareOutlined
|
||||
iconSize="m"
|
||||
style={{ marginTop: 2, marginLeft: 4 }}
|
||||
aria-label={t('Table actions')}
|
||||
/>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Title>
|
||||
{isMetadataRefreshing ? (
|
||||
<Skeleton active />
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import Alert, { AlertProps } from 'src/components/Alert';
|
||||
|
||||
type AlertType = Pick<AlertProps, 'type'>;
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import AlteredSliceTag, {
|
||||
alterForComparison,
|
||||
formatValueHandler,
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import '@testing-library/jest-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
|
||||
import { ModifiedInfo } from '.';
|
||||
|
||||
@@ -40,7 +43,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: 'dashed',
|
||||
tertiary: 'default',
|
||||
dashed: 'dashed',
|
||||
link: 'link',
|
||||
};
|
||||
|
||||
@@ -16,10 +16,17 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import { render, waitFor } from 'spec/helpers/testing-library';
|
||||
import Card from '.';
|
||||
|
||||
test('should render', () => {
|
||||
const { container } = render(<Card />);
|
||||
expect(container).toBeInTheDocument();
|
||||
afterEach(async () => {
|
||||
// Wait for any pending effects to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
test('should render', async () => {
|
||||
const { container } = render(<Card />);
|
||||
await waitFor(() => {
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import CertifiedBadge, {
|
||||
CertifiedBadgeProps,
|
||||
} from 'src/components/CertifiedBadge';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user