mirror of
https://github.com/apache/superset.git
synced 2026-06-29 19:35:33 +00:00
Compare commits
64 Commits
fire-alert
...
chore/ci/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32284d56a | ||
|
|
6bc77fecc2 | ||
|
|
420a74b01e | ||
|
|
7ba59c2d79 | ||
|
|
b77c525d4b | ||
|
|
41ce9ca7d3 | ||
|
|
c2fb94cedf | ||
|
|
1d0866556f | ||
|
|
b4dfeef2fd | ||
|
|
0ec6cae45d | ||
|
|
d6ede99861 | ||
|
|
9b6d3ce775 | ||
|
|
c1f4062af6 | ||
|
|
3bc3f47d67 | ||
|
|
acb996a324 | ||
|
|
c1d08bf27c | ||
|
|
d3d5297025 | ||
|
|
b1470bd5a5 | ||
|
|
18fea37e84 | ||
|
|
1b71c105b7 | ||
|
|
b061b5d317 | ||
|
|
386893f9f2 | ||
|
|
c1787a67aa | ||
|
|
dee5859599 | ||
|
|
1d3daf2ac8 | ||
|
|
9d56b1721d | ||
|
|
67182e255c | ||
|
|
e2c6dc3e1a | ||
|
|
c539ae98ba | ||
|
|
ca3c420412 | ||
|
|
5e8a0c0244 | ||
|
|
90fa31f305 | ||
|
|
5731d0874a | ||
|
|
66f5ab2d2f | ||
|
|
36b0ed023b | ||
|
|
3ff90bd532 | ||
|
|
5d06438a07 | ||
|
|
eb0d4dd601 | ||
|
|
92109f0f99 | ||
|
|
9431381c3e | ||
|
|
b94f90e39e | ||
|
|
714c5cd075 | ||
|
|
c65c0951cf | ||
|
|
ae5c08b993 | ||
|
|
b9c61a079d | ||
|
|
2599bea0c2 | ||
|
|
6c70f3d275 | ||
|
|
da893462b8 | ||
|
|
18853c6ecf | ||
|
|
8768e5be0f | ||
|
|
133473d0f4 | ||
|
|
5916ec4876 | ||
|
|
36781fbf47 | ||
|
|
215b207ae4 | ||
|
|
3b46a5f121 | ||
|
|
416fa266d9 | ||
|
|
f70a2eac89 | ||
|
|
c49391ab08 | ||
|
|
0fbace5b5d | ||
|
|
c55c85f824 | ||
|
|
e34b7c2daf | ||
|
|
eac5bd23bd | ||
|
|
27a65257ee | ||
|
|
932bb2f154 |
4
.github/workflows/bump-python-package.yml
vendored
4
.github/workflows/bump-python-package.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
|||||||
checks: write
|
checks: write
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
ref: master
|
ref: master
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-supersetbot/
|
uses: ./.github/actions/setup-supersetbot/
|
||||||
|
|
||||||
- name: Set up Python ${{ inputs.python-version }}
|
- name: Set up Python ${{ inputs.python-version }}
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/check-python-deps.yml
vendored
2
.github/workflows/check-python-deps.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Check and notify
|
- name: Check and notify
|
||||||
|
|||||||
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
frontend: ${{ steps.check.outputs.frontend }}
|
frontend: ${{ steps.check.outputs.frontend }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/dependency-review.yml
vendored
4
.github/workflows/dependency-review.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout Repository"
|
- name: "Checkout Repository"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: "Dependency Review"
|
- name: "Dependency Review"
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout Repository"
|
- name: "Checkout Repository"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
docker: ${{ steps.check.outputs.docker }}
|
docker: ${{ steps.check.outputs.docker }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Free up disk space
|
- name: Free up disk space
|
||||||
|
|||||||
2
.github/workflows/embedded-sdk-release.yml
vendored
2
.github/workflows/embedded-sdk-release.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: superset-embedded-sdk
|
working-directory: superset-embedded-sdk
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# Note: registry-url is intentionally omitted. When set, actions/setup-node
|
# Note: registry-url is intentionally omitted. When set, actions/setup-node
|
||||||
|
|||||||
2
.github/workflows/embedded-sdk-test.yml
vendored
2
.github/workflows/embedded-sdk-test.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: superset-embedded-sdk
|
working-directory: superset-embedded-sdk
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
|
|||||||
4
.github/workflows/generate-FOSSA-report.yml
vendored
4
.github/workflows/generate-FOSSA-report.yml
vendored
@@ -32,12 +32,12 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
java-version: "11"
|
java-version: "11"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/issue_creation.yml
vendored
2
.github/workflows/issue_creation.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/latest-release-tag.yml
vendored
2
.github/workflows/latest-release-tag.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
4
.github/workflows/license-check.yml
vendored
4
.github/workflows/license-check.yml
vendored
@@ -18,12 +18,12 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
java-version: "11"
|
java-version: "11"
|
||||||
|
|||||||
2
.github/workflows/pr-lint.yml
vendored
2
.github/workflows/pr-lint.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
|
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# pulls all commits (needed for lerna / semantic release to correctly version)
|
# pulls all commits (needed for lerna / semantic release to correctly version)
|
||||||
|
|||||||
2
.github/workflows/showtime-trigger.yml
vendored
2
.github/workflows/showtime-trigger.yml
vendored
@@ -152,7 +152,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout PR code (only if build needed)
|
- name: Checkout PR code (only if build needed)
|
||||||
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
|
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.check.outputs.target_sha }}
|
ref: ${{ steps.check.outputs.target_sha }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|||||||
2
.github/workflows/superset-app-cli.yml
vendored
2
.github/workflows/superset-app-cli.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
- 16379:6379
|
- 16379:6379
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
4
.github/workflows/superset-docs-deploy.yml
vendored
4
.github/workflows/superset-docs-deploy.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
|
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
node-version-file: "./docs/.nvmrc"
|
node-version-file: "./docs/.nvmrc"
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: ./.github/actions/setup-backend/
|
uses: ./.github/actions/setup-backend/
|
||||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||||
with:
|
with:
|
||||||
distribution: "zulu"
|
distribution: "zulu"
|
||||||
java-version: "21"
|
java-version: "21"
|
||||||
|
|||||||
6
.github/workflows/superset-docs-verify.yml
vendored
6
.github/workflows/superset-docs-verify.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
name: Link Checking
|
name: Link Checking
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# Do not bump this linkinator-action version without opening
|
# Do not bump this linkinator-action version without opening
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
working-directory: docs
|
working-directory: docs
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
working-directory: docs
|
working-directory: docs
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}"
|
- name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.workflow_run.head_sha }}
|
ref: ${{ github.event.workflow_run.head_sha }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|||||||
14
.github/workflows/superset-e2e.yml
vendored
14
.github/workflows/superset-e2e.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
frontend: ${{ steps.check.outputs.frontend }}
|
frontend: ${{ steps.check.outputs.frontend }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
@@ -97,21 +97,21 @@ jobs:
|
|||||||
# Conditional checkout based on context
|
# Conditional checkout based on context
|
||||||
- name: Checkout for push or pull_request event
|
- name: Checkout for push or pull_request event
|
||||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||||
- name: Checkout using ref (workflow_dispatch)
|
- name: Checkout using ref (workflow_dispatch)
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ github.event.inputs.ref }}
|
ref: ${{ github.event.inputs.ref }}
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Checkout using PR ID (workflow_dispatch)
|
- name: Checkout using PR ID (workflow_dispatch)
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||||
@@ -207,21 +207,21 @@ jobs:
|
|||||||
# Conditional checkout based on context (same as Cypress workflow)
|
# Conditional checkout based on context (same as Cypress workflow)
|
||||||
- name: Checkout for push or pull_request event
|
- name: Checkout for push or pull_request event
|
||||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||||
- name: Checkout using ref (workflow_dispatch)
|
- name: Checkout using ref (workflow_dispatch)
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ github.event.inputs.ref }}
|
ref: ${{ github.event.inputs.ref }}
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Checkout using PR ID (workflow_dispatch)
|
- name: Checkout using PR ID (workflow_dispatch)
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
working-directory: superset-extensions-cli
|
working-directory: superset-extensions-cli
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
4
.github/workflows/superset-frontend.yml
vendored
4
.github/workflows/superset-frontend.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
should-run: ${{ steps.check.outputs.frontend }}
|
should-run: ${{ steps.check.outputs.frontend }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
2
.github/workflows/superset-helm-lint.yml
vendored
2
.github/workflows/superset-helm-lint.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
2
.github/workflows/superset-helm-release.yml
vendored
2
.github/workflows/superset-helm-release.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.ref || github.ref_name }}
|
ref: ${{ inputs.ref || github.ref_name }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|||||||
8
.github/workflows/superset-playwright.yml
vendored
8
.github/workflows/superset-playwright.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
frontend: ${{ steps.check.outputs.frontend }}
|
frontend: ${{ steps.check.outputs.frontend }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
@@ -83,21 +83,21 @@ jobs:
|
|||||||
# Conditional checkout based on context (same as Cypress workflow)
|
# Conditional checkout based on context (same as Cypress workflow)
|
||||||
- name: Checkout for push or pull_request event
|
- name: Checkout for push or pull_request event
|
||||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||||
- name: Checkout using ref (workflow_dispatch)
|
- name: Checkout using ref (workflow_dispatch)
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ github.event.inputs.ref }}
|
ref: ${{ github.event.inputs.ref }}
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Checkout using PR ID (workflow_dispatch)
|
- name: Checkout using PR ID (workflow_dispatch)
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
python: ${{ steps.check.outputs.python }}
|
python: ${{ steps.check.outputs.python }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
- 16379:6379
|
- 16379:6379
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -157,7 +157,7 @@ jobs:
|
|||||||
- 16379:6379
|
- 16379:6379
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -207,7 +207,7 @@ jobs:
|
|||||||
- 16379:6379
|
- 16379:6379
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
python: ${{ steps.check.outputs.python }}
|
python: ${{ steps.check.outputs.python }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
- 16379:6379
|
- 16379:6379
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -127,7 +127,7 @@ jobs:
|
|||||||
- 16379:6379
|
- 16379:6379
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
python: ${{ steps.check.outputs.python }}
|
python: ${{ steps.check.outputs.python }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
PYTHONPATH: ${{ github.workspace }}
|
PYTHONPATH: ${{ github.workspace }}
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
4
.github/workflows/superset-translations.yml
vendored
4
.github/workflows/superset-translations.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|||||||
2
.github/workflows/superset-websocket.yml
vendored
2
.github/workflows/superset-websocket.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
2
.github/workflows/supersetbot.yml
vendored
2
.github/workflows/supersetbot.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
});
|
});
|
||||||
|
|
||||||
- name: "Checkout ( ${{ github.sha }} )"
|
- name: "Checkout ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
# zizmor: ignore[artipacked] - required persisted credentials to push synced requirement changes back to remote
|
# zizmor: ignore[artipacked] - required persisted credentials to push synced requirement changes back to remote
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
|
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|||||||
4
.github/workflows/tag-release.yml
vendored
4
.github/workflows/tag-release.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
2
.github/workflows/tech-debt.yml
vendored
2
.github/workflows/tech-debt.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
name: Generate Reports
|
name: Generate Reports
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ categories:
|
|||||||
- name: Clark.de
|
- name: Clark.de
|
||||||
url: https://clark.de/
|
url: https://clark.de/
|
||||||
|
|
||||||
|
- name: Cover Genius
|
||||||
|
url: https://covergenius.com/
|
||||||
|
|
||||||
- name: EnquiryLabs
|
- name: EnquiryLabs
|
||||||
url: https://www.enquirylabs.co.uk
|
url: https://www.enquirylabs.co.uk
|
||||||
|
|
||||||
@@ -92,6 +95,10 @@ categories:
|
|||||||
- name: KarrotPay
|
- name: KarrotPay
|
||||||
url: https://www.daangnpay.com/
|
url: https://www.daangnpay.com/
|
||||||
|
|
||||||
|
- name: NICE Actimize
|
||||||
|
url: https://www.niceactimize.com/
|
||||||
|
contributors: ["@stevensuting"]
|
||||||
|
|
||||||
- name: Remita
|
- name: Remita
|
||||||
url: https://remita.net
|
url: https://remita.net
|
||||||
contributors: ["@mujibishola"]
|
contributors: ["@mujibishola"]
|
||||||
@@ -112,9 +119,6 @@ categories:
|
|||||||
url: https://xendit.co/
|
url: https://xendit.co/
|
||||||
contributors: ["@LieAlbertTriAdrian"]
|
contributors: ["@LieAlbertTriAdrian"]
|
||||||
|
|
||||||
- name: Cover Genius
|
|
||||||
url: https://covergenius.com/
|
|
||||||
|
|
||||||
Gaming:
|
Gaming:
|
||||||
- name: Popoko VM Games Studio
|
- name: Popoko VM Games Studio
|
||||||
url: https://popoko.live
|
url: https://popoko.live
|
||||||
@@ -296,7 +300,6 @@ categories:
|
|||||||
logo: hifadih.png
|
logo: hifadih.png
|
||||||
contributors: ["@saintLaurent00"]
|
contributors: ["@saintLaurent00"]
|
||||||
|
|
||||||
# Logo approved by @anmol-hpe on behalf of HPE
|
|
||||||
- name: HPE
|
- name: HPE
|
||||||
url: https://www.hpe.com/in/en/home.html
|
url: https://www.hpe.com/in/en/home.html
|
||||||
logo: hpe.png
|
logo: hpe.png
|
||||||
@@ -429,6 +432,10 @@ categories:
|
|||||||
logo: userguiding.svg
|
logo: userguiding.svg
|
||||||
contributors: ["@tzercin"]
|
contributors: ["@tzercin"]
|
||||||
|
|
||||||
|
- name: Value Ad
|
||||||
|
url: https://bestpair.info/
|
||||||
|
contributors: ["@stevensuting"]
|
||||||
|
|
||||||
- name: Virtuoso QA
|
- name: Virtuoso QA
|
||||||
url: https://www.virtuosoqa.com
|
url: https://www.virtuosoqa.com
|
||||||
|
|
||||||
@@ -513,10 +520,6 @@ categories:
|
|||||||
url: https://www.sunbird.org/
|
url: https://www.sunbird.org/
|
||||||
contributors: ["@eksteporg"]
|
contributors: ["@eksteporg"]
|
||||||
|
|
||||||
- name: The GRAPH Network
|
|
||||||
url: https://thegraphnetwork.org/
|
|
||||||
contributors: ["@fccoelho"]
|
|
||||||
|
|
||||||
- name: Udemy
|
- name: Udemy
|
||||||
url: https://www.udemy.com/
|
url: https://www.udemy.com/
|
||||||
contributors: ["@sungjuly"]
|
contributors: ["@sungjuly"]
|
||||||
@@ -525,7 +528,24 @@ categories:
|
|||||||
url: https://www.vipkid.com.cn/
|
url: https://www.vipkid.com.cn/
|
||||||
contributors: ["@illpanda"]
|
contributors: ["@illpanda"]
|
||||||
|
|
||||||
- name: WikiMedia Foundation
|
Social Organization:
|
||||||
|
- name: Living Goods
|
||||||
|
url: https://www.livinggoods.org
|
||||||
|
contributors: ["@chelule"]
|
||||||
|
|
||||||
|
- name: One Acre Fund
|
||||||
|
url: https://oneacrefund.org/
|
||||||
|
contributors: ["@stevensuting"]
|
||||||
|
|
||||||
|
- name: Quest Alliance
|
||||||
|
url: https://www.questalliance.net/
|
||||||
|
contributors: ["@stevensuting"]
|
||||||
|
|
||||||
|
- name: The GRAPH Network
|
||||||
|
url: https://thegraphnetwork.org/
|
||||||
|
contributors: ["@fccoelho"]
|
||||||
|
|
||||||
|
- name: Wikimedia Foundation
|
||||||
url: https://wikimediafoundation.org
|
url: https://wikimediafoundation.org
|
||||||
contributors: ["@vg"]
|
contributors: ["@vg"]
|
||||||
|
|
||||||
@@ -538,6 +558,10 @@ categories:
|
|||||||
url: https://www.douroeci.com/
|
url: https://www.douroeci.com/
|
||||||
contributors: ["@nunohelibeires"]
|
contributors: ["@nunohelibeires"]
|
||||||
|
|
||||||
|
- name: Rogow
|
||||||
|
url: https://rogow.com.br/
|
||||||
|
contributors: ["@nilmonto"]
|
||||||
|
|
||||||
- name: Safaricom
|
- name: Safaricom
|
||||||
url: https://www.safaricom.co.ke/
|
url: https://www.safaricom.co.ke/
|
||||||
contributors: ["@mmutiso"]
|
contributors: ["@mmutiso"]
|
||||||
@@ -550,11 +574,10 @@ categories:
|
|||||||
url: https://wattbewerb.de/
|
url: https://wattbewerb.de/
|
||||||
contributors: ["@wattbewerb"]
|
contributors: ["@wattbewerb"]
|
||||||
|
|
||||||
- name: Rogow
|
|
||||||
url: https://rogow.com.br/
|
|
||||||
contributors: ["@nilmonto"]
|
|
||||||
|
|
||||||
Healthcare:
|
Healthcare:
|
||||||
|
- name: 2070Health
|
||||||
|
url: https://2070health.com/
|
||||||
|
|
||||||
- name: Amino
|
- name: Amino
|
||||||
url: https://amino.com
|
url: https://amino.com
|
||||||
contributors: ["@shkr"]
|
contributors: ["@shkr"]
|
||||||
@@ -567,10 +590,6 @@ categories:
|
|||||||
url: https://www.getcare.io/
|
url: https://www.getcare.io/
|
||||||
contributors: ["@alandao2021"]
|
contributors: ["@alandao2021"]
|
||||||
|
|
||||||
- name: Living Goods
|
|
||||||
url: https://www.livinggoods.org
|
|
||||||
contributors: ["@chelule"]
|
|
||||||
|
|
||||||
- name: Maieutical Labs
|
- name: Maieutical Labs
|
||||||
url: https://maieuticallabs.it
|
url: https://maieuticallabs.it
|
||||||
contributors: ["@xrmx"]
|
contributors: ["@xrmx"]
|
||||||
@@ -589,10 +608,10 @@ categories:
|
|||||||
- name: WeSure
|
- name: WeSure
|
||||||
url: https://www.wesure.cn/
|
url: https://www.wesure.cn/
|
||||||
|
|
||||||
- name: 2070Health
|
|
||||||
url: https://2070health.com/
|
|
||||||
|
|
||||||
HR / Staffing:
|
HR / Staffing:
|
||||||
|
- name: bluquist
|
||||||
|
url: https://bluquist.com/
|
||||||
|
|
||||||
- name: Swile
|
- name: Swile
|
||||||
url: https://www.swile.co/
|
url: https://www.swile.co/
|
||||||
contributors: ["@PaoloTerzi"]
|
contributors: ["@PaoloTerzi"]
|
||||||
@@ -600,21 +619,18 @@ categories:
|
|||||||
- name: Symmetrics
|
- name: Symmetrics
|
||||||
url: https://www.symmetrics.fyi
|
url: https://www.symmetrics.fyi
|
||||||
|
|
||||||
- name: bluquist
|
|
||||||
url: https://bluquist.com/
|
|
||||||
|
|
||||||
Government:
|
Government:
|
||||||
- name: City of Ann Arbor, MI
|
- name: City of Ann Arbor, MI
|
||||||
url: https://www.a2gov.org/
|
url: https://www.a2gov.org/
|
||||||
contributors: ["@sfirke"]
|
contributors: ["@sfirke"]
|
||||||
|
|
||||||
|
- name: NRLM - Sarathi, India
|
||||||
|
url: https://pib.gov.in/PressReleasePage.aspx?PRID=1999586
|
||||||
|
|
||||||
- name: RIS3 Strategy of CZ, MIT CR
|
- name: RIS3 Strategy of CZ, MIT CR
|
||||||
url: https://www.ris3.cz/
|
url: https://www.ris3.cz/
|
||||||
contributors: ["@RIS3CZ"]
|
contributors: ["@RIS3CZ"]
|
||||||
|
|
||||||
- name: NRLM - Sarathi, India
|
|
||||||
url: https://pib.gov.in/PressReleasePage.aspx?PRID=1999586
|
|
||||||
|
|
||||||
Mobile Software:
|
Mobile Software:
|
||||||
- name: VLMedia
|
- name: VLMedia
|
||||||
url: https://www.vlmedia.com.tr
|
url: https://www.vlmedia.com.tr
|
||||||
|
|||||||
@@ -72,20 +72,23 @@ services:
|
|||||||
- -c
|
- -c
|
||||||
- |
|
- |
|
||||||
url="http://host.docker.internal:9000/static/assets/manifest.json"
|
url="http://host.docker.internal:9000/static/assets/manifest.json"
|
||||||
max_attempts=150 # ~5 minutes at 2s intervals
|
max_attempts=300 # ~10 minutes at 2s intervals; first build can be slow
|
||||||
echo "Waiting for webpack dev server at $url..."
|
echo "Waiting for webpack dev server at $$url..."
|
||||||
attempt=0
|
attempt=0
|
||||||
until curl -sf --max-time 5 -o /dev/null "$url"; do
|
until curl -sf --max-time 5 -H "Host: localhost" -o /dev/null "$$url"; do
|
||||||
attempt=$((attempt + 1))
|
attempt=$$((attempt + 1))
|
||||||
if [ "$attempt" -ge "$max_attempts" ]; then
|
if [ "$$attempt" -ge "$$max_attempts" ]; then
|
||||||
echo "ERROR: webpack dev server did not serve $url after $max_attempts attempts (~5 minutes)." >&2
|
echo "ERROR: webpack dev server did not serve $$url after $$max_attempts attempts." >&2
|
||||||
echo "Is the dev server running? With BUILD_SUPERSET_FRONTEND_IN_DOCKER=false you must start it on the host (e.g. 'npm run dev' in superset-frontend)." >&2
|
echo "Is the dev server running? With BUILD_SUPERSET_FRONTEND_IN_DOCKER=false you must start it on the host (e.g. 'npm run dev' in superset-frontend)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [ $$((attempt % 15)) -eq 0 ]; then
|
||||||
|
echo "Still waiting for webpack dev server... ($$attempt/$$max_attempts)"
|
||||||
|
fi
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
echo "Webpack dev server is ready; starting nginx."
|
echo "Webpack dev server is ready; starting nginx."
|
||||||
exec nginx -g 'daemon off;'
|
exec /docker-entrypoint.sh nginx -g 'daemon off;'
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7
|
image: redis:7
|
||||||
|
|||||||
@@ -81,17 +81,19 @@ case "${1}" in
|
|||||||
app)
|
app)
|
||||||
echo "Starting web app (using development server)..."
|
echo "Starting web app (using development server)..."
|
||||||
|
|
||||||
# Environment-based debugger control for security
|
# Default to Flask debug mode in this dev compose entrypoint so the Talisman
|
||||||
# Only enable Werkzeug interactive debugger when explicitly requested
|
# dev CSP (which permits 'unsafe-eval' required by React Refresh / HMR) is
|
||||||
# Modern Werkzeug (3.0+) includes PIN protection, but defense-in-depth approach
|
# served. Operators can still set FLASK_DEBUG=false in docker/.env-local
|
||||||
# Override FLASK_DEBUG so the effective state matches SUPERSET_DEBUG_ENABLED even
|
# to exercise the production-like CSP and error handling.
|
||||||
# when FLASK_DEBUG=true is inherited from docker/.env or .flaskenv
|
: "${FLASK_DEBUG:=1}"
|
||||||
|
export FLASK_DEBUG
|
||||||
|
|
||||||
|
# Werkzeug's interactive debugger (/console) is a separate, security-sensitive
|
||||||
|
# feature and must be opted into explicitly via SUPERSET_DEBUG_ENABLED=true.
|
||||||
if [[ "${SUPERSET_DEBUG_ENABLED:-}" == "true" ]]; then
|
if [[ "${SUPERSET_DEBUG_ENABLED:-}" == "true" ]]; then
|
||||||
export FLASK_DEBUG=1
|
|
||||||
DEBUGGER_FLAG="--debugger"
|
DEBUGGER_FLAG="--debugger"
|
||||||
echo " ⚠️ Werkzeug debugger enabled (requires PIN for /console access)"
|
echo " ⚠️ Werkzeug debugger enabled (requires PIN for /console access)"
|
||||||
else
|
else
|
||||||
export FLASK_DEBUG=0
|
|
||||||
DEBUGGER_FLAG="--no-debugger"
|
DEBUGGER_FLAG="--no-debugger"
|
||||||
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
|
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
#
|
#
|
||||||
HYPHEN_SYMBOL='-'
|
HYPHEN_SYMBOL='-'
|
||||||
|
|
||||||
gunicorn \
|
exec gunicorn \
|
||||||
--bind "${SUPERSET_BIND_ADDRESS:-0.0.0.0}:${SUPERSET_PORT:-8088}" \
|
--bind "${SUPERSET_BIND_ADDRESS:-0.0.0.0}:${SUPERSET_PORT:-8088}" \
|
||||||
--access-logfile "${ACCESS_LOG_FILE:-$HYPHEN_SYMBOL}" \
|
--access-logfile "${ACCESS_LOG_FILE:-$HYPHEN_SYMBOL}" \
|
||||||
--error-logfile "${ERROR_LOG_FILE:-$HYPHEN_SYMBOL}" \
|
--error-logfile "${ERROR_LOG_FILE:-$HYPHEN_SYMBOL}" \
|
||||||
|
|||||||
@@ -28,14 +28,19 @@
|
|||||||
# Skip builds when no docs changes (exit 0 = skip, non-zero = build).
|
# Skip builds when no docs changes (exit 0 = skip, non-zero = build).
|
||||||
# Checks for changes in docs/ and README.md (which gets pulled into docs).
|
# Checks for changes in docs/ and README.md (which gets pulled into docs).
|
||||||
#
|
#
|
||||||
# $CACHED_COMMIT_REF is the last *deployed* commit. On a PR's first build it
|
# $CACHED_COMMIT_REF is the last *deployed* commit; it is set on incremental
|
||||||
# is empty, so the original `git diff` errored and Netlify fell back to
|
# builds (notably the master production deploy) and empty on a context's
|
||||||
# building -- which is why every PR built a docs preview once even with no
|
# first build (every deploy preview). The production path diffs against it
|
||||||
# docs changes. When it is empty we instead diff the whole branch against its
|
# and skips correctly.
|
||||||
# merge-base with master, so non-docs PRs are skipped from the very first
|
#
|
||||||
# build. Subsequent builds (and the master production build) keep the cheaper
|
# Deploy previews need different handling: Netlify checks out a *merge*
|
||||||
# incremental $CACHED_COMMIT_REF diff. Any failure exits non-zero -> build.
|
# commit, so $COMMIT_REF (the PR head SHA) is frequently not resolvable in
|
||||||
ignore = 'if [ -n "$CACHED_COMMIT_REF" ]; then git diff --quiet "$CACHED_COMMIT_REF" "$COMMIT_REF" -- . ../README.md; else git fetch origin master --depth=100 >/dev/null 2>&1; git diff --quiet "$(git merge-base origin/master "$COMMIT_REF" 2>/dev/null || echo origin/master)" "$COMMIT_REF" -- . ../README.md; fi'
|
# the clone, and on a shallow clone `git merge-base` can fail too -- so the
|
||||||
|
# previous logic fell through to a build on every PR, even non-docs ones.
|
||||||
|
# Instead, always diff the checked-out HEAD against its merge-base with
|
||||||
|
# master, deepening the shallow clone until that merge-base resolves. If it
|
||||||
|
# genuinely can't be determined, exit non-zero to build (fail safe).
|
||||||
|
ignore = 'if [ -n "$CACHED_COMMIT_REF" ]; then git diff --quiet "$CACHED_COMMIT_REF" HEAD -- . ../README.md; else git fetch --no-tags origin master >/dev/null 2>&1 || true; i=0; while [ "$i" -lt 10 ] && ! git merge-base origin/master HEAD >/dev/null 2>&1; do git fetch --deepen=200 origin master >/dev/null 2>&1 || break; i=$((i+1)); done; BASE="$(git merge-base origin/master HEAD 2>/dev/null || true)"; if [ -z "$BASE" ]; then exit 1; fi; git diff --quiet "$BASE" HEAD -- . ../README.md; fi'
|
||||||
|
|
||||||
[build.environment]
|
[build.environment]
|
||||||
# Node version matching docs/.nvmrc
|
# Node version matching docs/.nvmrc
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"prettier": "^3.8.4",
|
"prettier": "^3.8.4",
|
||||||
"typescript": "~6.0.3",
|
"typescript": "~6.0.3",
|
||||||
"typescript-eslint": "^8.61.0",
|
"typescript-eslint": "^8.61.1",
|
||||||
"webpack": "^5.107.2"
|
"webpack": "^5.107.2"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|||||||
@@ -1808,6 +1808,10 @@ If you enable DML in the meta database users will be able to run DML queries on
|
|||||||
|
|
||||||
Second, you might want to change the value of `SUPERSET_META_DB_LIMIT`. The default value is 1000, and defines how many are read from each database before any aggregations and joins are executed. You can also set this value `None` if you only have small tables.
|
Second, you might want to change the value of `SUPERSET_META_DB_LIMIT`. The default value is 1000, and defines how many are read from each database before any aggregations and joins are executed. You can also set this value `None` if you only have small tables.
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
`SUPERSET_META_DB_LIMIT` is applied to **each** underlying table *before* the in-memory join runs, not to the final result. If any table involved in a join has more rows than the limit, the meta database will read only the first `SUPERSET_META_DB_LIMIT` rows of that table, which means matching rows can be silently dropped and the join can return **incomplete or even empty** results with no error. If you join tables larger than the limit, raise `SUPERSET_META_DB_LIMIT` to comfortably exceed your largest joined table, or set it to `None` when working only with small tables, to get correct results.
|
||||||
|
:::
|
||||||
|
|
||||||
Additionally, you might want to restrict the databases to with the meta database has access to. This can be done in the database configuration, under "Advanced" -> "Other" -> "ENGINE PARAMETERS" and adding:
|
Additionally, you might want to restrict the databases to with the meta database has access to. This can be done in the database configuration, under "Advanced" -> "Other" -> "ENGINE PARAMETERS" and adding:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
150
docs/yarn.lock
150
docs/yarn.lock
@@ -4932,110 +4932,110 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/yargs-parser" "*"
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@8.61.0", "@typescript-eslint/eslint-plugin@^8.59.3":
|
"@typescript-eslint/eslint-plugin@8.61.1", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||||
version "8.61.0"
|
version "8.61.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz#db20271974b94a3a54d3b9544e5f5b3481448400"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz#6e4b7fee21f1983308e9e9b634ecbaf702c86006"
|
||||||
integrity sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==
|
integrity sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint-community/regexpp" "^4.12.2"
|
"@eslint-community/regexpp" "^4.12.2"
|
||||||
"@typescript-eslint/scope-manager" "8.61.0"
|
"@typescript-eslint/scope-manager" "8.61.1"
|
||||||
"@typescript-eslint/type-utils" "8.61.0"
|
"@typescript-eslint/type-utils" "8.61.1"
|
||||||
"@typescript-eslint/utils" "8.61.0"
|
"@typescript-eslint/utils" "8.61.1"
|
||||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
"@typescript-eslint/visitor-keys" "8.61.1"
|
||||||
ignore "^7.0.5"
|
ignore "^7.0.5"
|
||||||
natural-compare "^1.4.0"
|
natural-compare "^1.4.0"
|
||||||
ts-api-utils "^2.5.0"
|
ts-api-utils "^2.5.0"
|
||||||
|
|
||||||
"@typescript-eslint/parser@8.61.0", "@typescript-eslint/parser@^8.61.0":
|
"@typescript-eslint/parser@8.61.1", "@typescript-eslint/parser@^8.61.0":
|
||||||
version "8.61.0"
|
version "8.61.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.0.tgz#1afe73c9ccce16b7a26d6b95f9400b0ccc34af87"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.1.tgz#881fba60b50636249cdeea2e547bf75715254c72"
|
||||||
integrity sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==
|
integrity sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/scope-manager" "8.61.0"
|
"@typescript-eslint/scope-manager" "8.61.1"
|
||||||
"@typescript-eslint/types" "8.61.0"
|
"@typescript-eslint/types" "8.61.1"
|
||||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
"@typescript-eslint/typescript-estree" "8.61.1"
|
||||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
"@typescript-eslint/visitor-keys" "8.61.1"
|
||||||
debug "^4.4.3"
|
debug "^4.4.3"
|
||||||
|
|
||||||
"@typescript-eslint/project-service@8.61.0":
|
"@typescript-eslint/project-service@8.61.1":
|
||||||
version "8.61.0"
|
version "8.61.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz#417a2feac32e8ebd336d63f068c3b42b736ea1ac"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.1.tgz#fcd9739964a40867eed55f1ac318d3909f24b4af"
|
||||||
integrity sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==
|
integrity sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/tsconfig-utils" "^8.61.0"
|
"@typescript-eslint/tsconfig-utils" "^8.61.1"
|
||||||
"@typescript-eslint/types" "^8.61.0"
|
"@typescript-eslint/types" "^8.61.1"
|
||||||
debug "^4.4.3"
|
debug "^4.4.3"
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager@8.61.0":
|
"@typescript-eslint/scope-manager@8.61.1":
|
||||||
version "8.61.0"
|
version "8.61.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz#93c2520d05653fe65eb9ee98efc74fd0134a7852"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz#2479921a40fdb0afa18f5838fae6167264b417b2"
|
||||||
integrity sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==
|
integrity sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "8.61.0"
|
"@typescript-eslint/types" "8.61.1"
|
||||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
"@typescript-eslint/visitor-keys" "8.61.1"
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils@8.61.0":
|
"@typescript-eslint/tsconfig-utils@8.61.1":
|
||||||
version "8.61.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba"
|
|
||||||
integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==
|
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils@^8.61.0":
|
|
||||||
version "8.61.1"
|
version "8.61.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz#ca88080e0cf191d49516d7f300b67aa090d2254f"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz#ca88080e0cf191d49516d7f300b67aa090d2254f"
|
||||||
integrity sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==
|
integrity sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==
|
||||||
|
|
||||||
"@typescript-eslint/type-utils@8.61.0":
|
"@typescript-eslint/tsconfig-utils@^8.61.1":
|
||||||
version "8.61.0"
|
version "8.62.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz#50219b57e6b89cecfb1a15f093b15ec9ee019974"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz#9440a673581c6d9de308c4d5803dd52ed5d71729"
|
||||||
integrity sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==
|
integrity sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==
|
||||||
|
|
||||||
|
"@typescript-eslint/type-utils@8.61.1":
|
||||||
|
version "8.61.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz#8fa18f453ee140893b47d339d1a6b64cac9b08a1"
|
||||||
|
integrity sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "8.61.0"
|
"@typescript-eslint/types" "8.61.1"
|
||||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
"@typescript-eslint/typescript-estree" "8.61.1"
|
||||||
"@typescript-eslint/utils" "8.61.0"
|
"@typescript-eslint/utils" "8.61.1"
|
||||||
debug "^4.4.3"
|
debug "^4.4.3"
|
||||||
ts-api-utils "^2.5.0"
|
ts-api-utils "^2.5.0"
|
||||||
|
|
||||||
"@typescript-eslint/types@8.61.0":
|
"@typescript-eslint/types@8.61.1":
|
||||||
version "8.61.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d"
|
|
||||||
integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==
|
|
||||||
|
|
||||||
"@typescript-eslint/types@^8.61.0":
|
|
||||||
version "8.61.1"
|
version "8.61.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.1.tgz#0c51f518e4e6848371a1c988e859d59eb7522d5a"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.1.tgz#0c51f518e4e6848371a1c988e859d59eb7522d5a"
|
||||||
integrity sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==
|
integrity sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree@8.61.0":
|
"@typescript-eslint/types@^8.61.1":
|
||||||
version "8.61.0"
|
version "8.62.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz#98ca47260bbf627fc28f018b3a0abf00e3090690"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.0.tgz#601427c10203d9f0f34f0b3e474df735eb12b593"
|
||||||
integrity sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==
|
integrity sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree@8.61.1":
|
||||||
|
version "8.61.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz#febbe70365ac0bf7611262b61b338fc8797965c7"
|
||||||
|
integrity sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/project-service" "8.61.0"
|
"@typescript-eslint/project-service" "8.61.1"
|
||||||
"@typescript-eslint/tsconfig-utils" "8.61.0"
|
"@typescript-eslint/tsconfig-utils" "8.61.1"
|
||||||
"@typescript-eslint/types" "8.61.0"
|
"@typescript-eslint/types" "8.61.1"
|
||||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
"@typescript-eslint/visitor-keys" "8.61.1"
|
||||||
debug "^4.4.3"
|
debug "^4.4.3"
|
||||||
minimatch "^10.2.2"
|
minimatch "^10.2.2"
|
||||||
semver "^7.7.3"
|
semver "^7.7.3"
|
||||||
tinyglobby "^0.2.15"
|
tinyglobby "^0.2.15"
|
||||||
ts-api-utils "^2.5.0"
|
ts-api-utils "^2.5.0"
|
||||||
|
|
||||||
"@typescript-eslint/utils@8.61.0":
|
"@typescript-eslint/utils@8.61.1":
|
||||||
version "8.61.0"
|
version "8.61.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.1.tgz#ffd1054de7dd33b7873cd6c6713ec6b0366316d3"
|
||||||
integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==
|
integrity sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint-community/eslint-utils" "^4.9.1"
|
"@eslint-community/eslint-utils" "^4.9.1"
|
||||||
"@typescript-eslint/scope-manager" "8.61.0"
|
"@typescript-eslint/scope-manager" "8.61.1"
|
||||||
"@typescript-eslint/types" "8.61.0"
|
"@typescript-eslint/types" "8.61.1"
|
||||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
"@typescript-eslint/typescript-estree" "8.61.1"
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@8.61.0":
|
"@typescript-eslint/visitor-keys@8.61.1":
|
||||||
version "8.61.0"
|
version "8.61.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz#39b4e1ab8936d23bea973d39fd092f9aa21f275e"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz#546cf102b4efdb72a9a08e63a1b0d7d745eb66eb"
|
||||||
integrity sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==
|
integrity sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "8.61.0"
|
"@typescript-eslint/types" "8.61.1"
|
||||||
eslint-visitor-keys "^5.0.0"
|
eslint-visitor-keys "^5.0.0"
|
||||||
|
|
||||||
"@ungap/structured-clone@^1.0.0":
|
"@ungap/structured-clone@^1.0.0":
|
||||||
@@ -14502,15 +14502,15 @@ types-ramda@^0.30.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ts-toolbelt "^9.6.0"
|
ts-toolbelt "^9.6.0"
|
||||||
|
|
||||||
typescript-eslint@^8.61.0:
|
typescript-eslint@^8.61.1:
|
||||||
version "8.61.0"
|
version "8.61.1"
|
||||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz#6927fb94f5f29623e370d33fd9fa61f15d6d996b"
|
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.1.tgz#7c224a9a643b7f42d295c67a75c1e30fee8c3eaa"
|
||||||
integrity sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==
|
integrity sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/eslint-plugin" "8.61.0"
|
"@typescript-eslint/eslint-plugin" "8.61.1"
|
||||||
"@typescript-eslint/parser" "8.61.0"
|
"@typescript-eslint/parser" "8.61.1"
|
||||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
"@typescript-eslint/typescript-estree" "8.61.1"
|
||||||
"@typescript-eslint/utils" "8.61.0"
|
"@typescript-eslint/utils" "8.61.1"
|
||||||
|
|
||||||
typescript@~6.0.3:
|
typescript@~6.0.3:
|
||||||
version "6.0.3"
|
version "6.0.3"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ maintainers:
|
|||||||
- name: craig-rueda
|
- name: craig-rueda
|
||||||
email: craig@craigrueda.com
|
email: craig@craigrueda.com
|
||||||
url: https://github.com/craig-rueda
|
url: https://github.com/craig-rueda
|
||||||
version: 0.16.2 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
version: 0.17.2 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: postgresql
|
- name: postgresql
|
||||||
version: 16.7.27
|
version: 16.7.27
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
|||||||
|
|
||||||
# superset
|
# superset
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||||
|
|
||||||
@@ -111,9 +111,6 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
|||||||
| init.resources | object | `{}` | |
|
| init.resources | object | `{}` | |
|
||||||
| init.tolerations | list | `[]` | |
|
| init.tolerations | list | `[]` | |
|
||||||
| init.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to init job |
|
| init.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to init job |
|
||||||
| initImage.pullPolicy | string | `"IfNotPresent"` | |
|
|
||||||
| initImage.repository | string | `"apache/superset"` | |
|
|
||||||
| initImage.tag | string | `"dockerize"` | |
|
|
||||||
| nameOverride | string | `nil` | Provide a name to override the name of the chart |
|
| nameOverride | string | `nil` | Provide a name to override the name of the chart |
|
||||||
| nodeSelector | object | `{}` | |
|
| nodeSelector | object | `{}` | |
|
||||||
| postgresql | object | see `values.yaml` | Configuration values for the postgresql dependency. ref: https://github.com/bitnami/charts/tree/main/bitnami/postgresql |
|
| postgresql | object | see `values.yaml` | Configuration values for the postgresql dependency. ref: https://github.com/bitnami/charts/tree/main/bitnami/postgresql |
|
||||||
@@ -219,6 +216,7 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
|||||||
| supersetNode.extraContainers | list | `[]` | Launch additional containers into supersetNode pod |
|
| supersetNode.extraContainers | list | `[]` | Launch additional containers into supersetNode pod |
|
||||||
| supersetNode.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
|
| supersetNode.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
|
||||||
| supersetNode.initContainers | list | a container waiting for postgres | Init containers |
|
| supersetNode.initContainers | list | a container waiting for postgres | Init containers |
|
||||||
|
| supersetNode.lifecycle | object | `{}` | Container lifecycle hooks, e.g. a preStop sleep so the Service/Ingress stops routing to the pod before gunicorn receives SIGTERM |
|
||||||
| supersetNode.livenessProbe.failureThreshold | int | `3` | |
|
| supersetNode.livenessProbe.failureThreshold | int | `3` | |
|
||||||
| supersetNode.livenessProbe.httpGet.path | string | `"/health"` | |
|
| supersetNode.livenessProbe.httpGet.path | string | `"/health"` | |
|
||||||
| supersetNode.livenessProbe.httpGet.port | string | `"http"` | |
|
| supersetNode.livenessProbe.httpGet.port | string | `"http"` | |
|
||||||
@@ -251,6 +249,7 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
|||||||
| supersetNode.startupProbe.successThreshold | int | `1` | |
|
| supersetNode.startupProbe.successThreshold | int | `1` | |
|
||||||
| supersetNode.startupProbe.timeoutSeconds | int | `1` | |
|
| supersetNode.startupProbe.timeoutSeconds | int | `1` | |
|
||||||
| supersetNode.strategy | object | `{}` | |
|
| supersetNode.strategy | object | `{}` | |
|
||||||
|
| supersetNode.terminationGracePeriodSeconds | string | `nil` | Pod termination grace period (seconds). Set greater than GUNICORN_TIMEOUT so in-flight requests can drain before SIGKILL |
|
||||||
| supersetNode.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetNode deployments |
|
| supersetNode.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetNode deployments |
|
||||||
| supersetWebsockets.affinity | object | `{}` | Affinity to be added to supersetWebsockets deployment |
|
| supersetWebsockets.affinity | object | `{}` | Affinity to be added to supersetWebsockets deployment |
|
||||||
| supersetWebsockets.command | list | `[]` | |
|
| supersetWebsockets.command | list | `[]` | |
|
||||||
@@ -314,6 +313,7 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
|||||||
| supersetWorker.extraContainers | list | `[]` | Launch additional containers into supersetWorker pod |
|
| supersetWorker.extraContainers | list | `[]` | Launch additional containers into supersetWorker pod |
|
||||||
| supersetWorker.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
|
| supersetWorker.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
|
||||||
| supersetWorker.initContainers | list | a container waiting for postgres and redis | Init container |
|
| supersetWorker.initContainers | list | a container waiting for postgres and redis | Init container |
|
||||||
|
| supersetWorker.lifecycle | object | `{}` | Container lifecycle hooks for the worker pod |
|
||||||
| supersetWorker.livenessProbe.exec.command | list | a `celery inspect ping` command | Liveness probe command |
|
| supersetWorker.livenessProbe.exec.command | list | a `celery inspect ping` command | Liveness probe command |
|
||||||
| supersetWorker.livenessProbe.failureThreshold | int | `3` | |
|
| supersetWorker.livenessProbe.failureThreshold | int | `3` | |
|
||||||
| supersetWorker.livenessProbe.initialDelaySeconds | int | `120` | |
|
| supersetWorker.livenessProbe.initialDelaySeconds | int | `120` | |
|
||||||
@@ -334,6 +334,7 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
|||||||
| supersetWorker.resources | object | `{}` | Resource settings for the supersetWorker pods - these settings overwrite might existing values from the global resources object defined above. |
|
| supersetWorker.resources | object | `{}` | Resource settings for the supersetWorker pods - these settings overwrite might existing values from the global resources object defined above. |
|
||||||
| supersetWorker.startupProbe | object | `{}` | No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic) |
|
| supersetWorker.startupProbe | object | `{}` | No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic) |
|
||||||
| supersetWorker.strategy | object | `{}` | |
|
| supersetWorker.strategy | object | `{}` | |
|
||||||
|
| supersetWorker.terminationGracePeriodSeconds | string | `nil` | Pod termination grace period (seconds) for the worker pod so in-flight tasks can drain before SIGKILL |
|
||||||
| supersetWorker.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetWorker deployments |
|
| supersetWorker.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetWorker deployments |
|
||||||
| tolerations | list | `[]` | |
|
| tolerations | list | `[]` | |
|
||||||
| topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to all deployments |
|
| topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to all deployments |
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ spec:
|
|||||||
{{- if .Values.supersetWorker.livenessProbe }}
|
{{- if .Values.supersetWorker.livenessProbe }}
|
||||||
livenessProbe: {{- .Values.supersetWorker.livenessProbe | toYaml | nindent 12 }}
|
livenessProbe: {{- .Values.supersetWorker.livenessProbe | toYaml | nindent 12 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if .Values.supersetWorker.lifecycle }}
|
||||||
|
lifecycle: {{- .Values.supersetWorker.lifecycle | toYaml | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
resources:
|
resources:
|
||||||
{{- if .Values.supersetWorker.resources }}
|
{{- if .Values.supersetWorker.resources }}
|
||||||
{{- toYaml .Values.supersetWorker.resources | nindent 12 }}
|
{{- toYaml .Values.supersetWorker.resources | nindent 12 }}
|
||||||
@@ -170,6 +173,9 @@ spec:
|
|||||||
{{- with .Values.tolerations }}
|
{{- with .Values.tolerations }}
|
||||||
tolerations: {{- toYaml . | nindent 8 }}
|
tolerations: {{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if .Values.supersetWorker.terminationGracePeriodSeconds }}
|
||||||
|
terminationGracePeriodSeconds: {{ .Values.supersetWorker.terminationGracePeriodSeconds }}
|
||||||
|
{{- end }}
|
||||||
{{- if .Values.imagePullSecrets }}
|
{{- if .Values.imagePullSecrets }}
|
||||||
imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
|
imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ spec:
|
|||||||
{{- if .Values.supersetNode.livenessProbe }}
|
{{- if .Values.supersetNode.livenessProbe }}
|
||||||
livenessProbe: {{- .Values.supersetNode.livenessProbe | toYaml | nindent 12 }}
|
livenessProbe: {{- .Values.supersetNode.livenessProbe | toYaml | nindent 12 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if .Values.supersetNode.lifecycle }}
|
||||||
|
lifecycle: {{- .Values.supersetNode.lifecycle | toYaml | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
resources:
|
resources:
|
||||||
{{- if .Values.supersetNode.resources }}
|
{{- if .Values.supersetNode.resources }}
|
||||||
{{- toYaml .Values.supersetNode.resources | nindent 12 }}
|
{{- toYaml .Values.supersetNode.resources | nindent 12 }}
|
||||||
@@ -180,6 +183,9 @@ spec:
|
|||||||
{{- with .Values.tolerations }}
|
{{- with .Values.tolerations }}
|
||||||
tolerations: {{- toYaml . | nindent 8 }}
|
tolerations: {{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if .Values.supersetNode.terminationGracePeriodSeconds }}
|
||||||
|
terminationGracePeriodSeconds: {{ .Values.supersetNode.terminationGracePeriodSeconds }}
|
||||||
|
{{- end }}
|
||||||
{{- if .Values.imagePullSecrets }}
|
{{- if .Values.imagePullSecrets }}
|
||||||
imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
|
imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
@@ -194,11 +194,6 @@ image:
|
|||||||
|
|
||||||
imagePullSecrets: []
|
imagePullSecrets: []
|
||||||
|
|
||||||
initImage:
|
|
||||||
repository: apache/superset
|
|
||||||
tag: dockerize
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
|
|
||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 8088
|
port: 8088
|
||||||
@@ -274,7 +269,7 @@ supersetNode:
|
|||||||
command:
|
command:
|
||||||
- "/bin/sh"
|
- "/bin/sh"
|
||||||
- "-c"
|
- "-c"
|
||||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; /usr/bin/run-server.sh"
|
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec /usr/bin/run-server.sh"
|
||||||
connections:
|
connections:
|
||||||
# -- Change in case of bringing your own redis and then also set redis.enabled:false
|
# -- Change in case of bringing your own redis and then also set redis.enabled:false
|
||||||
redis_host: "{{ .Release.Name }}-redis-headless"
|
redis_host: "{{ .Release.Name }}-redis-headless"
|
||||||
@@ -303,15 +298,29 @@ supersetNode:
|
|||||||
# @default -- a container waiting for postgres
|
# @default -- a container waiting for postgres
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: wait-for-postgres
|
- name: wait-for-postgres
|
||||||
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
|
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: "{{ tpl .Values.envFromSecret . }}"
|
name: "{{ tpl .Values.envFromSecret . }}"
|
||||||
command:
|
command:
|
||||||
- /bin/sh
|
- /bin/bash
|
||||||
- -c
|
- -c
|
||||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s
|
- |
|
||||||
|
# opening a /dev/tcp fd performs a TCP connect without sending any
|
||||||
|
# payload (avoids postgres "incomplete startup packet" log noise);
|
||||||
|
# no external `dockerize`, `nc`, or busybox needed. SECONDS-based
|
||||||
|
# deadline mirrors the prior `dockerize -timeout 120s` behaviour.
|
||||||
|
SECONDS=0
|
||||||
|
until (exec 3<>/dev/tcp/"$DB_HOST"/"$DB_PORT") 2>/dev/null; do
|
||||||
|
if [ "$SECONDS" -ge 120 ]; then
|
||||||
|
echo "timeout waiting for postgres at $DB_HOST:$DB_PORT after 120s" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "waiting for postgres at $DB_HOST:$DB_PORT (elapsed ${SECONDS}s)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "postgres at $DB_HOST:$DB_PORT is up"
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: "256Mi"
|
memory: "256Mi"
|
||||||
@@ -360,6 +369,12 @@ supersetNode:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
periodSeconds: 15
|
periodSeconds: 15
|
||||||
successThreshold: 1
|
successThreshold: 1
|
||||||
|
# -- Container lifecycle hooks, e.g. a preStop sleep so the Service/Ingress
|
||||||
|
# stops routing to the pod before gunicorn receives SIGTERM
|
||||||
|
lifecycle: {}
|
||||||
|
# -- Pod termination grace period (seconds). Set greater than GUNICORN_TIMEOUT so
|
||||||
|
# in-flight requests can drain before SIGKILL
|
||||||
|
terminationGracePeriodSeconds: ~
|
||||||
# -- Resource settings for the supersetNode pods - these settings overwrite might existing values from the global resources object defined above.
|
# -- Resource settings for the supersetNode pods - these settings overwrite might existing values from the global resources object defined above.
|
||||||
resources: {}
|
resources: {}
|
||||||
# limits:
|
# limits:
|
||||||
@@ -400,22 +415,38 @@ supersetWorker:
|
|||||||
command:
|
command:
|
||||||
- "/bin/sh"
|
- "/bin/sh"
|
||||||
- "-c"
|
- "-c"
|
||||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; celery --app=superset.tasks.celery_app:app worker"
|
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec celery --app=superset.tasks.celery_app:app worker"
|
||||||
# -- If true, forces deployment to reload on each upgrade
|
# -- If true, forces deployment to reload on each upgrade
|
||||||
forceReload: false
|
forceReload: false
|
||||||
# -- Init container
|
# -- Init container
|
||||||
# @default -- a container waiting for postgres and redis
|
# @default -- a container waiting for postgres and redis
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: wait-for-postgres-redis
|
- name: wait-for-postgres-redis
|
||||||
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
|
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: "{{ tpl .Values.envFromSecret . }}"
|
name: "{{ tpl .Values.envFromSecret . }}"
|
||||||
command:
|
command:
|
||||||
- /bin/sh
|
- /bin/bash
|
||||||
- -c
|
- -c
|
||||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
|
- |
|
||||||
|
# See supersetNode.initContainers for the rationale.
|
||||||
|
SECONDS=0
|
||||||
|
wait_for() {
|
||||||
|
local host=$1 port=$2 name=$3
|
||||||
|
until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
|
||||||
|
if [ "$SECONDS" -ge 120 ]; then
|
||||||
|
echo "timeout waiting for $name at $host:$port after 120s" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "waiting for $name at $host:$port (elapsed ${SECONDS}s)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "$name at $host:$port is up"
|
||||||
|
}
|
||||||
|
wait_for "$DB_HOST" "$DB_PORT" postgres
|
||||||
|
wait_for "$REDIS_HOST" "$REDIS_PORT" redis
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: "256Mi"
|
memory: "256Mi"
|
||||||
@@ -464,6 +495,10 @@ supersetWorker:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
periodSeconds: 60
|
periodSeconds: 60
|
||||||
successThreshold: 1
|
successThreshold: 1
|
||||||
|
# -- Container lifecycle hooks for the worker pod
|
||||||
|
lifecycle: {}
|
||||||
|
# -- Pod termination grace period (seconds) for the worker pod so in-flight tasks can drain before SIGKILL
|
||||||
|
terminationGracePeriodSeconds: ~
|
||||||
# -- No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic)
|
# -- No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic)
|
||||||
startupProbe: {}
|
startupProbe: {}
|
||||||
# -- No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic)
|
# -- No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic)
|
||||||
@@ -488,22 +523,38 @@ supersetCeleryBeat:
|
|||||||
command:
|
command:
|
||||||
- "/bin/sh"
|
- "/bin/sh"
|
||||||
- "-c"
|
- "-c"
|
||||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; celery --app=superset.tasks.celery_app:app beat --pidfile /tmp/celerybeat.pid --schedule /tmp/celerybeat-schedule"
|
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec celery --app=superset.tasks.celery_app:app beat --pidfile /tmp/celerybeat.pid --schedule /tmp/celerybeat-schedule"
|
||||||
# -- If true, forces deployment to reload on each upgrade
|
# -- If true, forces deployment to reload on each upgrade
|
||||||
forceReload: false
|
forceReload: false
|
||||||
# -- List of init containers
|
# -- List of init containers
|
||||||
# @default -- a container waiting for postgres
|
# @default -- a container waiting for postgres
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: wait-for-postgres-redis
|
- name: wait-for-postgres-redis
|
||||||
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
|
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: "{{ tpl .Values.envFromSecret . }}"
|
name: "{{ tpl .Values.envFromSecret . }}"
|
||||||
command:
|
command:
|
||||||
- /bin/sh
|
- /bin/bash
|
||||||
- -c
|
- -c
|
||||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
|
- |
|
||||||
|
# See supersetNode.initContainers for the rationale.
|
||||||
|
SECONDS=0
|
||||||
|
wait_for() {
|
||||||
|
local host=$1 port=$2 name=$3
|
||||||
|
until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
|
||||||
|
if [ "$SECONDS" -ge 120 ]; then
|
||||||
|
echo "timeout waiting for $name at $host:$port after 120s" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "waiting for $name at $host:$port (elapsed ${SECONDS}s)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "$name at $host:$port is up"
|
||||||
|
}
|
||||||
|
wait_for "$DB_HOST" "$DB_PORT" postgres
|
||||||
|
wait_for "$REDIS_HOST" "$REDIS_PORT" redis
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: "256Mi"
|
memory: "256Mi"
|
||||||
@@ -594,15 +645,31 @@ supersetCeleryFlower:
|
|||||||
# @default -- a container waiting for postgres and redis
|
# @default -- a container waiting for postgres and redis
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: wait-for-postgres-redis
|
- name: wait-for-postgres-redis
|
||||||
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
|
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: "{{ tpl .Values.envFromSecret . }}"
|
name: "{{ tpl .Values.envFromSecret . }}"
|
||||||
command:
|
command:
|
||||||
- /bin/sh
|
- /bin/bash
|
||||||
- -c
|
- -c
|
||||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
|
- |
|
||||||
|
# See supersetNode.initContainers for the rationale.
|
||||||
|
SECONDS=0
|
||||||
|
wait_for() {
|
||||||
|
local host=$1 port=$2 name=$3
|
||||||
|
until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
|
||||||
|
if [ "$SECONDS" -ge 120 ]; then
|
||||||
|
echo "timeout waiting for $name at $host:$port after 120s" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "waiting for $name at $host:$port (elapsed ${SECONDS}s)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "$name at $host:$port is up"
|
||||||
|
}
|
||||||
|
wait_for "$DB_HOST" "$DB_PORT" postgres
|
||||||
|
wait_for "$REDIS_HOST" "$REDIS_PORT" redis
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: "256Mi"
|
memory: "256Mi"
|
||||||
@@ -764,15 +831,26 @@ init:
|
|||||||
# @default -- a container waiting for postgres
|
# @default -- a container waiting for postgres
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: wait-for-postgres
|
- name: wait-for-postgres
|
||||||
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
|
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: "{{ tpl .Values.envFromSecret . }}"
|
name: "{{ tpl .Values.envFromSecret . }}"
|
||||||
command:
|
command:
|
||||||
- /bin/sh
|
- /bin/bash
|
||||||
- -c
|
- -c
|
||||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s
|
- |
|
||||||
|
# See supersetNode.initContainers for the rationale.
|
||||||
|
SECONDS=0
|
||||||
|
until (exec 3<>/dev/tcp/"$DB_HOST"/"$DB_PORT") 2>/dev/null; do
|
||||||
|
if [ "$SECONDS" -ge 120 ]; then
|
||||||
|
echo "timeout waiting for postgres at $DB_HOST:$DB_PORT after 120s" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "waiting for postgres at $DB_HOST:$DB_PORT (elapsed ${SECONDS}s)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "postgres at $DB_HOST:$DB_PORT is up"
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: "256Mi"
|
memory: "256Mi"
|
||||||
|
|||||||
@@ -375,7 +375,6 @@ select = [
|
|||||||
|
|
||||||
ignore = [
|
ignore = [
|
||||||
"S101",
|
"S101",
|
||||||
"PT004", # Fixtures that don't return values - underscore prefix conflicts with pytest usage
|
|
||||||
"PT006",
|
"PT006",
|
||||||
"T201",
|
"T201",
|
||||||
"N999",
|
"N999",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ asyncio_mode = auto
|
|||||||
filterwarnings =
|
filterwarnings =
|
||||||
ignore
|
ignore
|
||||||
always::sqlalchemy.exc.RemovedIn20Warning
|
always::sqlalchemy.exc.RemovedIn20Warning
|
||||||
# error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
|
error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
|
||||||
# error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
# error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||||
# error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
# error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||||
# error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
# error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from flask import current_app
|
|||||||
from flask_appbuilder import Model
|
from flask_appbuilder import Model
|
||||||
from flask_migrate import downgrade, upgrade
|
from flask_migrate import downgrade, upgrade
|
||||||
from progress.bar import ChargingBar
|
from progress.bar import ChargingBar
|
||||||
from sqlalchemy import create_engine, inspect
|
from sqlalchemy import create_engine, inspect, text
|
||||||
from sqlalchemy.ext.automap import automap_base
|
from sqlalchemy.ext.automap import automap_base
|
||||||
|
|
||||||
from superset import db
|
from superset import db
|
||||||
@@ -154,7 +154,7 @@ def main( # noqa: C901
|
|||||||
|
|
||||||
print(f"Migration goes from {down_revision} to {revision}")
|
print(f"Migration goes from {down_revision} to {revision}")
|
||||||
current_revision = db.engine.execute(
|
current_revision = db.engine.execute(
|
||||||
"SELECT version_num FROM alembic_version"
|
text("SELECT version_num FROM alembic_version")
|
||||||
).scalar()
|
).scalar()
|
||||||
print(f"Current version of the DB is {current_revision}")
|
print(f"Current version of the DB is {current_revision}")
|
||||||
|
|
||||||
|
|||||||
765
superset-frontend/package-lock.json
generated
765
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -119,7 +119,7 @@
|
|||||||
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
|
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
|
||||||
"@jsonforms/core": "^3.7.0",
|
"@jsonforms/core": "^3.7.0",
|
||||||
"@jsonforms/react": "^3.7.0",
|
"@jsonforms/react": "^3.7.0",
|
||||||
"@jsonforms/vanilla-renderers": "^3.7.0",
|
"@jsonforms/vanilla-renderers": "^3.8.0",
|
||||||
"@luma.gl/constants": "~9.2.5",
|
"@luma.gl/constants": "~9.2.5",
|
||||||
"@luma.gl/core": "~9.2.5",
|
"@luma.gl/core": "~9.2.5",
|
||||||
"@luma.gl/engine": "~9.2.5",
|
"@luma.gl/engine": "~9.2.5",
|
||||||
@@ -260,17 +260,17 @@
|
|||||||
"@babel/types": "^7.29.7",
|
"@babel/types": "^7.29.7",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/jest": "^11.14.2",
|
"@emotion/jest": "^11.14.2",
|
||||||
"@formatjs/intl-durationformat": "^0.10.14",
|
"@formatjs/intl-durationformat": "^0.10.15",
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||||
"@playwright/test": "^1.60.0",
|
"@playwright/test": "^1.61.0",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||||
"@storybook/addon-docs": "10.4.4",
|
"@storybook/addon-docs": "10.4.5",
|
||||||
"@storybook/addon-links": "10.4.4",
|
"@storybook/addon-links": "10.4.4",
|
||||||
"@storybook/react-webpack5": "10.4.4",
|
"@storybook/react-webpack5": "10.4.4",
|
||||||
"@storybook/test-runner": "0.24.4",
|
"@storybook/test-runner": "0.24.4",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@swc/core": "^1.15.41",
|
"@swc/core": "^1.15.41",
|
||||||
"@swc/plugin-emotion": "^14.12.0",
|
"@swc/plugin-emotion": "^14.13.0",
|
||||||
"@swc/plugin-transform-imports": "^12.5.0",
|
"@swc/plugin-transform-imports": "^12.5.0",
|
||||||
"@testing-library/dom": "^9.3.4",
|
"@testing-library/dom": "^9.3.4",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
@@ -296,7 +296,7 @@
|
|||||||
"@types/rison": "0.1.0",
|
"@types/rison": "0.1.0",
|
||||||
"@types/tinycolor2": "^1.4.3",
|
"@types/tinycolor2": "^1.4.3",
|
||||||
"@types/unzipper": "^0.10.11",
|
"@types/unzipper": "^0.10.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
"@typescript-eslint/eslint-plugin": "^8.61.1",
|
||||||
"@typescript-eslint/parser": "^8.61.0",
|
"@typescript-eslint/parser": "^8.61.0",
|
||||||
"babel-jest": "^30.4.1",
|
"babel-jest": "^30.4.1",
|
||||||
"babel-loader": "^10.1.1",
|
"babel-loader": "^10.1.1",
|
||||||
@@ -323,8 +323,8 @@
|
|||||||
"eslint-plugin-no-only-tests": "^3.4.0",
|
"eslint-plugin-no-only-tests": "^3.4.0",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.0",
|
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||||
"eslint-plugin-storybook": "10.4.4",
|
"eslint-plugin-storybook": "10.4.5",
|
||||||
"eslint-plugin-testing-library": "^7.16.2",
|
"eslint-plugin-testing-library": "^7.16.2",
|
||||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||||
"fetch-mock": "^12.6.0",
|
"fetch-mock": "^12.6.0",
|
||||||
@@ -343,7 +343,7 @@
|
|||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"mini-css-extract-plugin": "^2.10.2",
|
"mini-css-extract-plugin": "^2.10.2",
|
||||||
"open-cli": "^9.0.0",
|
"open-cli": "^9.0.0",
|
||||||
"oxlint": "^1.69.0",
|
"oxlint": "^1.70.0",
|
||||||
"po2json": "^0.4.5",
|
"po2json": "^0.4.5",
|
||||||
"prettier": "3.8.4",
|
"prettier": "3.8.4",
|
||||||
"prettier-plugin-packagejson": "^3.0.2",
|
"prettier-plugin-packagejson": "^3.0.2",
|
||||||
@@ -355,7 +355,7 @@
|
|||||||
"source-map": "^0.7.6",
|
"source-map": "^0.7.6",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"speed-measure-webpack-plugin": "^1.6.0",
|
"speed-measure-webpack-plugin": "^1.6.0",
|
||||||
"storybook": "10.4.4",
|
"storybook": "10.4.6",
|
||||||
"style-loader": "^4.0.0",
|
"style-loader": "^4.0.0",
|
||||||
"swc-loader": "^0.2.7",
|
"swc-loader": "^0.2.7",
|
||||||
"terser-webpack-plugin": "^5.6.1",
|
"terser-webpack-plugin": "^5.6.1",
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ export function transformLinkUri(uri: string): string {
|
|||||||
// "java\tscript:" or "java\x01script:") are ignored by browsers, so strip
|
// "java\tscript:" or "java\x01script:") are ignored by browsers, so strip
|
||||||
// them before comparing against the blocklist.
|
// them before comparing against the blocklist.
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
const scheme = url.slice(0, colon).replace(/[\u0000-\u0020]/g, '').toLowerCase();
|
const scheme = url
|
||||||
|
.slice(0, colon)
|
||||||
|
.replace(/[\u0000-\u0020]/g, '')
|
||||||
|
.toLowerCase();
|
||||||
return DANGEROUS_LINK_PROTOCOLS.includes(scheme) ? '' : url;
|
return DANGEROUS_LINK_PROTOCOLS.includes(scheme) ? '' : url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -519,7 +519,8 @@ const Select = forwardRef(
|
|||||||
handleSelectAll();
|
handleSelectAll();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('Select all')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
|
{t('Select all')}{' '}
|
||||||
|
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
@@ -536,7 +537,8 @@ const Select = forwardRef(
|
|||||||
handleDeselectAll();
|
handleDeselectAll();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('Clear')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
|
{t('Clear')}{' '}
|
||||||
|
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
|
||||||
</Button>
|
</Button>
|
||||||
</StyledBulkActionsContainer>
|
</StyledBulkActionsContainer>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -97,8 +97,11 @@ testWithAssets(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// At least one list item should contain a DD.MM.YYYY formatted date.
|
// At least one list item should contain a DD.MM.YYYY formatted date.
|
||||||
await expect(panel.locator('li').first()).toHaveText(/\d{2}\.\d{2}\.\d{4}/, {
|
await expect(panel.locator('li').first()).toHaveText(
|
||||||
timeout: TIMEOUT.API_RESPONSE,
|
/\d{2}\.\d{2}\.\d{4}/,
|
||||||
});
|
{
|
||||||
|
timeout: TIMEOUT.API_RESPONSE,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -182,10 +182,7 @@ testWithAssets(
|
|||||||
// Now track POST /api/v1/chart/data requests around Clear All
|
// Now track POST /api/v1/chart/data requests around Clear All
|
||||||
const postsAfterClearAll: string[] = [];
|
const postsAfterClearAll: string[] = [];
|
||||||
const handler = (req: any) => {
|
const handler = (req: any) => {
|
||||||
if (
|
if (req.url().includes('/api/v1/chart/data') && req.method() === 'POST') {
|
||||||
req.url().includes('/api/v1/chart/data') &&
|
|
||||||
req.method() === 'POST'
|
|
||||||
) {
|
|
||||||
postsAfterClearAll.push(req.url());
|
postsAfterClearAll.push(req.url());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,7 +109,12 @@ testWithAssets(
|
|||||||
id: chartLayoutKey,
|
id: chartLayoutKey,
|
||||||
children: [],
|
children: [],
|
||||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
|
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
|
||||||
meta: { chartId, width: 8, height: 60, sliceName: 'mixed_filter_repro' },
|
meta: {
|
||||||
|
chartId,
|
||||||
|
width: 8,
|
||||||
|
height: 60,
|
||||||
|
sliceName: 'mixed_filter_repro',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const jsonMetadata = {
|
const jsonMetadata = {
|
||||||
@@ -130,9 +135,7 @@ testWithAssets(
|
|||||||
defaultDataMask: {
|
defaultDataMask: {
|
||||||
filterState: { value: [FILTER_VALUE] },
|
filterState: { value: [FILTER_VALUE] },
|
||||||
extraFormData: {
|
extraFormData: {
|
||||||
filters: [
|
filters: [{ col: FILTER_COLUMN, op: 'IN', val: [FILTER_VALUE] }],
|
||||||
{ col: FILTER_COLUMN, op: 'IN', val: [FILTER_VALUE] },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cascadeParentIds: [],
|
cascadeParentIds: [],
|
||||||
@@ -158,15 +161,14 @@ testWithAssets(
|
|||||||
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
|
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
|
||||||
testAssets.trackDashboard(dashboardId);
|
testAssets.trackDashboard(dashboardId);
|
||||||
|
|
||||||
await apiPut(page, `api/v1/chart/${chartId}`, { dashboards: [dashboardId] });
|
await apiPut(page, `api/v1/chart/${chartId}`, {
|
||||||
|
dashboards: [dashboardId],
|
||||||
|
});
|
||||||
|
|
||||||
// Capture the Mixed chart's data request (the one with two queries).
|
// Capture the Mixed chart's data request (the one with two queries).
|
||||||
const twoQueryPayloads: any[] = [];
|
const twoQueryPayloads: any[] = [];
|
||||||
page.on('request', req => {
|
page.on('request', req => {
|
||||||
if (
|
if (req.url().includes('/api/v1/chart/data') && req.method() === 'POST') {
|
||||||
req.url().includes('/api/v1/chart/data') &&
|
|
||||||
req.method() === 'POST'
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const body = req.postDataJSON();
|
const body = req.postDataJSON();
|
||||||
if (body?.queries?.length === 2) {
|
if (body?.queries?.length === 2) {
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ import {
|
|||||||
getGuestToken,
|
getGuestToken,
|
||||||
} from '../../helpers/api/embedded';
|
} from '../../helpers/api/embedded';
|
||||||
import { apiPost, apiPut } from '../../helpers/api/requests';
|
import { apiPost, apiPut } from '../../helpers/api/requests';
|
||||||
import { apiPostDashboard, apiDeleteDashboard } from '../../helpers/api/dashboard';
|
import {
|
||||||
|
apiPostDashboard,
|
||||||
|
apiDeleteDashboard,
|
||||||
|
} from '../../helpers/api/dashboard';
|
||||||
import { apiDeleteChart } from '../../helpers/api/chart';
|
import { apiDeleteChart } from '../../helpers/api/chart';
|
||||||
import { EmbeddedPage } from '../../pages/EmbeddedPage';
|
import { EmbeddedPage } from '../../pages/EmbeddedPage';
|
||||||
import { EMBEDDED } from '../../utils/constants';
|
import { EMBEDDED } from '../../utils/constants';
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
ControlPanelConfig,
|
ControlPanelConfig,
|
||||||
D3_FORMAT_DOCS,
|
D3_FORMAT_DOCS,
|
||||||
D3_TIME_FORMAT_OPTIONS,
|
D3_TIME_FORMAT_OPTIONS,
|
||||||
|
DEFAULT_TIME_FORMAT,
|
||||||
getStandardizedControls,
|
getStandardizedControls,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
@@ -145,7 +146,7 @@ const config: ControlPanelConfig = {
|
|||||||
freeForm: true,
|
freeForm: true,
|
||||||
label: t('Time Format'),
|
label: t('Time Format'),
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
default: 'smart_date',
|
default: DEFAULT_TIME_FORMAT,
|
||||||
choices: D3_TIME_FORMAT_OPTIONS,
|
choices: D3_TIME_FORMAT_OPTIONS,
|
||||||
description: D3_FORMAT_DOCS,
|
description: D3_FORMAT_DOCS,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,10 +21,12 @@
|
|||||||
import d3 from 'd3';
|
import d3 from 'd3';
|
||||||
import { extent as d3Extent } from 'd3-array';
|
import { extent as d3Extent } from 'd3-array';
|
||||||
import {
|
import {
|
||||||
ValueFormatter,
|
BinaryQueryObjectFilterClause,
|
||||||
getNumberFormatter,
|
|
||||||
getSequentialSchemeRegistry,
|
|
||||||
CategoricalColorNamespace,
|
CategoricalColorNamespace,
|
||||||
|
ContextMenuFilters,
|
||||||
|
DataMask,
|
||||||
|
ValueFormatter,
|
||||||
|
getSequentialSchemeRegistry,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import countries, { countryOptions } from './countries';
|
import countries, { countryOptions } from './countries';
|
||||||
|
|
||||||
@@ -65,9 +67,28 @@ interface CountryMapProps {
|
|||||||
formatter: ValueFormatter;
|
formatter: ValueFormatter;
|
||||||
colorScheme: string;
|
colorScheme: string;
|
||||||
sliceId: number;
|
sliceId: number;
|
||||||
|
onContextMenu?: (
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
data: ContextMenuFilters,
|
||||||
|
) => void;
|
||||||
|
emitCrossFilters?: boolean;
|
||||||
|
setDataMask?: (dataMask: DataMask) => void;
|
||||||
|
filterState?: {
|
||||||
|
selectedValues?: string[];
|
||||||
|
extraFormData?: {
|
||||||
|
filters?: BinaryQueryObjectFilterClause[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
entity?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maps: Record<string, GeoData> = {};
|
const maps: Record<string, GeoData> = {};
|
||||||
|
// Store zoom state per chart instance using element as key to enable garbage collection
|
||||||
|
const zoomStates = new WeakMap<
|
||||||
|
HTMLElement,
|
||||||
|
{ scale: number; translate: [number, number] }
|
||||||
|
>();
|
||||||
|
|
||||||
function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||||
const {
|
const {
|
||||||
@@ -75,10 +96,15 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
country,
|
country,
|
||||||
|
entity,
|
||||||
linearColorScheme,
|
linearColorScheme,
|
||||||
formatter,
|
formatter,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
sliceId,
|
sliceId,
|
||||||
|
filterState,
|
||||||
|
emitCrossFilters,
|
||||||
|
onContextMenu,
|
||||||
|
setDataMask,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const container = element;
|
const container = element;
|
||||||
@@ -99,7 +125,15 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
|||||||
? colorScale(d.country_id, sliceId)
|
? colorScale(d.country_id, sliceId)
|
||||||
: (linearColorScale(d.metric) ?? '');
|
: (linearColorScale(d.metric) ?? '');
|
||||||
});
|
});
|
||||||
const colorFn = (d: GeoFeature) => colorMap[d.properties.ISO] || 'none';
|
|
||||||
|
const colorFn = (feature: GeoFeature): string => {
|
||||||
|
if (!feature?.properties) return '#d9d9d9';
|
||||||
|
const iso = feature.properties.ISO;
|
||||||
|
return colorMap[iso] || '#d9d9d9';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if dashboard is in edit mode
|
||||||
|
const isEditMode = container.closest('.dashboard--editing') !== null;
|
||||||
|
|
||||||
const path = d3.geo.path();
|
const path = d3.geo.path();
|
||||||
const div = d3.select(container);
|
const div = d3.select(container);
|
||||||
@@ -112,6 +146,11 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
|||||||
.attr('width', width)
|
.attr('width', width)
|
||||||
.attr('height', height)
|
.attr('height', height)
|
||||||
.attr('preserveAspectRatio', 'xMidYMid meet');
|
.attr('preserveAspectRatio', 'xMidYMid meet');
|
||||||
|
|
||||||
|
// Only set grab cursor if not in edit mode
|
||||||
|
if (!isEditMode) {
|
||||||
|
svg.style('cursor', 'grab');
|
||||||
|
}
|
||||||
const backgroundRect = svg
|
const backgroundRect = svg
|
||||||
.append('rect')
|
.append('rect')
|
||||||
.attr('class', 'background')
|
.attr('class', 'background')
|
||||||
@@ -119,39 +158,64 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
|||||||
.attr('height', height);
|
.attr('height', height);
|
||||||
const g = svg.append('g');
|
const g = svg.append('g');
|
||||||
const mapLayer = g.append('g').classed('map-layer', true);
|
const mapLayer = g.append('g').classed('map-layer', true);
|
||||||
|
// Add hover popup for tooltip
|
||||||
const hoverPopup = div.append('div').attr('class', 'hover-popup');
|
const hoverPopup = div.append('div').attr('class', 'hover-popup');
|
||||||
|
|
||||||
let centered: GeoFeature | null;
|
// Track mouse position to distinguish clicks from drags
|
||||||
|
let mousedownPos: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
const clicked = function clicked(d: GeoFeature) {
|
// Cross-filter support
|
||||||
const hasCenter = d && centered !== d;
|
const getCrossFilterDataMask = (
|
||||||
let x: number;
|
source: GeoFeature,
|
||||||
let y: number;
|
): { dataMask: DataMask; isCurrentValueSelected: boolean } | undefined => {
|
||||||
let k: number;
|
if (!entity) return undefined;
|
||||||
const halfWidth = width / 2;
|
|
||||||
const halfHeight = height / 2;
|
|
||||||
|
|
||||||
if (hasCenter) {
|
const selected = filterState?.selectedValues || [];
|
||||||
const centroid = path.centroid(d);
|
const iso = source?.properties?.ISO;
|
||||||
[x, y] = centroid;
|
if (!iso) return undefined;
|
||||||
k = 4;
|
|
||||||
centered = d;
|
|
||||||
} else {
|
|
||||||
x = halfWidth;
|
|
||||||
y = halfHeight;
|
|
||||||
k = 1;
|
|
||||||
centered = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
g.transition()
|
const isSelected = selected.includes(iso);
|
||||||
.duration(750)
|
const values = isSelected ? [] : [iso];
|
||||||
.attr(
|
|
||||||
'transform',
|
return {
|
||||||
`translate(${halfWidth},${halfHeight})scale(${k})translate(${-x},${-y})`,
|
dataMask: {
|
||||||
);
|
extraFormData: {
|
||||||
|
filters: values.length
|
||||||
|
? [{ col: entity, op: 'IN', val: values }]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
filterState: {
|
||||||
|
value: values.length ? values : null,
|
||||||
|
selectedValues: values.length ? values : null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isCurrentValueSelected: isSelected,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
backgroundRect.on('click', clicked);
|
// Handle right-click context menu
|
||||||
|
const handleContextMenu = (feature: GeoFeature): void => {
|
||||||
|
const pointerEvent = d3.event;
|
||||||
|
|
||||||
|
if (typeof onContextMenu === 'function') {
|
||||||
|
pointerEvent?.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
const iso = feature?.properties?.ISO;
|
||||||
|
if (!iso || typeof onContextMenu !== 'function' || !entity) return;
|
||||||
|
|
||||||
|
const drillVal = iso;
|
||||||
|
const drillToDetailFilters = [
|
||||||
|
{ col: entity, op: '==', val: drillVal, formattedVal: drillVal },
|
||||||
|
];
|
||||||
|
const drillByFilters = [{ col: entity, op: '==', val: drillVal }];
|
||||||
|
|
||||||
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||||
|
drillToDetail: drillToDetailFilters,
|
||||||
|
crossFilter: getCrossFilterDataMask(feature),
|
||||||
|
drillBy: { filters: drillByFilters, groupbyFieldName: 'entity' },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getNameOfRegion = function getNameOfRegion(
|
const getNameOfRegion = function getNameOfRegion(
|
||||||
feature: GeoFeature,
|
feature: GeoFeature,
|
||||||
@@ -165,7 +229,7 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePopupPosition = () => {
|
const updatePopupPosition = (): void => {
|
||||||
const svgHeight = svg.node().getBoundingClientRect().height;
|
const svgHeight = svg.node().getBoundingClientRect().height;
|
||||||
const [x, y] = d3.mouse(svg.node());
|
const [x, y] = d3.mouse(svg.node());
|
||||||
hoverPopup
|
hoverPopup
|
||||||
@@ -175,34 +239,135 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
|||||||
.classed('popup-at-bottom', y > (svgHeight * 2) / 3);
|
.classed('popup-at-bottom', y > (svgHeight * 2) / 3);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mouseenter = function mouseenter(this: SVGPathElement, d: GeoFeature) {
|
const mouseenter = function mouseenter(
|
||||||
|
this: SVGPathElement,
|
||||||
|
d: GeoFeature,
|
||||||
|
): void {
|
||||||
// Darken color
|
// Darken color
|
||||||
let c: string = colorFn(d);
|
let c: string = colorFn(d);
|
||||||
if (c !== 'none') {
|
if (c) {
|
||||||
c = d3.rgb(c).darker().toString();
|
c = d3.rgb(c).darker().toString();
|
||||||
}
|
}
|
||||||
d3.select(this).style('fill', c);
|
d3.select(this).style('fill', c);
|
||||||
|
|
||||||
// Display information popup
|
// Display information popup
|
||||||
const result = data.filter(
|
const result = data.filter(r => r.country_id === d?.properties?.ISO);
|
||||||
region => region.country_id === d.properties.ISO,
|
const regionName = escapeHtml(getNameOfRegion(d));
|
||||||
);
|
const metricValue =
|
||||||
|
result.length > 0 ? escapeHtml(String(formatter(result[0].metric))) : '';
|
||||||
hoverPopup.style('display', 'block').html(
|
hoverPopup
|
||||||
`<div><strong>${getNameOfRegion(d)}</strong><br>${result.length > 0 ? formatter(result[0].metric) : ''}</div>`,
|
.style('display', 'block')
|
||||||
);
|
.html(`<div><strong>${regionName}</strong><br>${metricValue}</div>`);
|
||||||
updatePopupPosition();
|
updatePopupPosition();
|
||||||
};
|
};
|
||||||
|
|
||||||
const mousemove = function mousemove() {
|
// Mouse move handler to update tooltip position
|
||||||
|
const mousemove = function mousemove(): void {
|
||||||
updatePopupPosition();
|
updatePopupPosition();
|
||||||
};
|
};
|
||||||
|
|
||||||
const mouseout = function mouseout(this: SVGPathElement) {
|
const mouseout = function mouseout(this: SVGPathElement): void {
|
||||||
d3.select(this).style('fill', colorFn);
|
d3.select(this).style('fill', (d: GeoFeature) => colorFn(d));
|
||||||
hoverPopup.style('display', 'none');
|
hoverPopup.style('display', 'none');
|
||||||
};
|
};
|
||||||
|
|
||||||
function drawMap(mapData: GeoData) {
|
// Only enable zoom if not in edit mode
|
||||||
|
if (!isEditMode) {
|
||||||
|
// Zoom with panning bounds
|
||||||
|
const zoom = d3.behavior
|
||||||
|
.zoom()
|
||||||
|
.scaleExtent([1, 4])
|
||||||
|
.on('zoomstart', () => {
|
||||||
|
svg.style('cursor', 'grabbing');
|
||||||
|
})
|
||||||
|
.on('zoom', () => {
|
||||||
|
const { translate, scale } = d3.event;
|
||||||
|
let [tx, ty] = translate;
|
||||||
|
|
||||||
|
const scaledW = width * scale;
|
||||||
|
const scaledH = height * scale;
|
||||||
|
const minX = Math.min(0, width - scaledW);
|
||||||
|
const maxX = 0;
|
||||||
|
const minY = Math.min(0, height - scaledH);
|
||||||
|
const maxY = 0;
|
||||||
|
|
||||||
|
tx = Math.max(Math.min(tx, maxX), minX);
|
||||||
|
ty = Math.max(Math.min(ty, maxY), minY);
|
||||||
|
|
||||||
|
// Sync D3's internal translate state with the clamped values so the
|
||||||
|
// next wheel/zoom event starts from the constrained position rather
|
||||||
|
// than the unclamped one (otherwise the view jumps).
|
||||||
|
zoom.translate([tx, ty]);
|
||||||
|
|
||||||
|
g.attr('transform', `translate(${tx}, ${ty}) scale(${scale})`);
|
||||||
|
const prev = zoomStates.get(element);
|
||||||
|
const changed =
|
||||||
|
!prev ||
|
||||||
|
prev.scale !== scale ||
|
||||||
|
prev.translate[0] !== tx ||
|
||||||
|
prev.translate[1] !== ty;
|
||||||
|
if (changed) {
|
||||||
|
zoomStates.set(element, { scale, translate: [tx, ty] });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('zoomend', () => {
|
||||||
|
svg.style('cursor', 'grab');
|
||||||
|
});
|
||||||
|
|
||||||
|
d3.select(svg.node()).call(zoom);
|
||||||
|
|
||||||
|
// Restore previous zoom state if it exists
|
||||||
|
const savedZoom = zoomStates.get(element);
|
||||||
|
if (savedZoom) {
|
||||||
|
const { scale, translate } = savedZoom;
|
||||||
|
zoom.scale(scale).translate(translate);
|
||||||
|
g.attr(
|
||||||
|
'transform',
|
||||||
|
`translate(${translate[0]}, ${translate[1]}) scale(${scale})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visual highlighting for selected regions
|
||||||
|
function highlightSelectedRegion(
|
||||||
|
selectedValues: string[] | null = null,
|
||||||
|
): void {
|
||||||
|
const selected = selectedValues || filterState?.selectedValues || [];
|
||||||
|
|
||||||
|
mapLayer
|
||||||
|
.selectAll('path.region')
|
||||||
|
.style('fill-opacity', (d: GeoFeature) => {
|
||||||
|
const iso = d?.properties?.ISO;
|
||||||
|
return selected.length === 0 || selected.includes(iso) ? 1 : 0.3;
|
||||||
|
})
|
||||||
|
.style('stroke', (d: GeoFeature) => {
|
||||||
|
const iso = d?.properties?.ISO;
|
||||||
|
return selected.includes(iso) ? '#222' : null;
|
||||||
|
})
|
||||||
|
.style('stroke-width', (d: GeoFeature) => {
|
||||||
|
const iso = d?.properties?.ISO;
|
||||||
|
return selected.includes(iso) ? '1.5px' : '0.5px';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click handler for cross-filters
|
||||||
|
const handleClick = (feature: GeoFeature): void => {
|
||||||
|
if (!entity || !emitCrossFilters || typeof setDataMask !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = getCrossFilterDataMask(feature);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const { dataMask, isCurrentValueSelected } = result;
|
||||||
|
setDataMask(dataMask);
|
||||||
|
|
||||||
|
const iso = feature?.properties?.ISO;
|
||||||
|
const newSelection = isCurrentValueSelected || !iso ? [] : [iso];
|
||||||
|
highlightSelectedRegion(newSelection);
|
||||||
|
};
|
||||||
|
|
||||||
|
function drawMap(mapData: GeoData): void {
|
||||||
const { features } = mapData;
|
const { features } = mapData;
|
||||||
const center = d3.geo.centroid(mapData);
|
const center = d3.geo.centroid(mapData);
|
||||||
const scale = 100;
|
const scale = 100;
|
||||||
@@ -213,13 +378,11 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
|||||||
.translate([width / 2, height / 2]);
|
.translate([width / 2, height / 2]);
|
||||||
path.projection(projection);
|
path.projection(projection);
|
||||||
|
|
||||||
// Compute scale that fits container.
|
|
||||||
const bounds = path.bounds(mapData);
|
const bounds = path.bounds(mapData);
|
||||||
const hscale = (scale * width) / (bounds[1][0] - bounds[0][0]);
|
const hscale = (scale * width) / (bounds[1][0] - bounds[0][0]);
|
||||||
const vscale = (scale * height) / (bounds[1][1] - bounds[0][1]);
|
const vscale = (scale * height) / (bounds[1][1] - bounds[0][1]);
|
||||||
const newScale = hscale < vscale ? hscale : vscale;
|
const newScale = Math.min(hscale, vscale);
|
||||||
|
|
||||||
// Compute bounds and offset using the updated scale.
|
|
||||||
projection.scale(newScale);
|
projection.scale(newScale);
|
||||||
const newBounds = path.bounds(mapData);
|
const newBounds = path.bounds(mapData);
|
||||||
projection.translate([
|
projection.translate([
|
||||||
@@ -227,20 +390,45 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
|||||||
height - (newBounds[0][1] + newBounds[1][1]) / 2,
|
height - (newBounds[0][1] + newBounds[1][1]) / 2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Draw each province as a path
|
const sel = mapLayer.selectAll('path.region').data(features);
|
||||||
mapLayer
|
|
||||||
.selectAll('path')
|
sel
|
||||||
.data(features)
|
|
||||||
.enter()
|
.enter()
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr('d', path)
|
|
||||||
.attr('class', 'region')
|
.attr('class', 'region')
|
||||||
.attr('vector-effect', 'non-scaling-stroke')
|
.attr('vector-effect', 'non-scaling-stroke');
|
||||||
|
|
||||||
|
// Apply attributes and event handlers to all elements (enter + update)
|
||||||
|
mapLayer
|
||||||
|
.selectAll('path.region')
|
||||||
|
.attr('d', path)
|
||||||
.style('fill', colorFn)
|
.style('fill', colorFn)
|
||||||
.on('mouseenter', mouseenter)
|
.on('mouseenter', mouseenter)
|
||||||
.on('mousemove', mousemove)
|
.on('mousemove', mousemove)
|
||||||
.on('mouseout', mouseout)
|
.on('mouseout', mouseout)
|
||||||
.on('click', clicked);
|
.on('contextmenu', handleContextMenu)
|
||||||
|
.on('mousedown', function mousedown() {
|
||||||
|
const pos = d3.mouse(svg.node());
|
||||||
|
mousedownPos = { x: pos[0], y: pos[1] };
|
||||||
|
})
|
||||||
|
.on('click', function click(feature: GeoFeature) {
|
||||||
|
if (mousedownPos) {
|
||||||
|
const pos = d3.mouse(svg.node());
|
||||||
|
const dx = Math.abs(pos[0] - mousedownPos.x);
|
||||||
|
const dy = Math.abs(pos[1] - mousedownPos.y);
|
||||||
|
const dragThreshold = 5;
|
||||||
|
|
||||||
|
if (dx < dragThreshold && dy < dragThreshold) {
|
||||||
|
handleClick(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedownPos = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sel.exit().remove();
|
||||||
|
|
||||||
|
highlightSelectedRegion();
|
||||||
}
|
}
|
||||||
|
|
||||||
const map = maps[country];
|
const map = maps[country];
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
import { ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||||
import transformProps from './transformProps';
|
import transformProps from './transformProps';
|
||||||
import exampleUsa from './images/exampleUsa.jpg';
|
import exampleUsa from './images/exampleUsa.jpg';
|
||||||
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
||||||
@@ -49,6 +49,11 @@ const metadata = new ChartMetadata({
|
|||||||
thumbnail,
|
thumbnail,
|
||||||
thumbnailDark,
|
thumbnailDark,
|
||||||
useLegacyApi: true,
|
useLegacyApi: true,
|
||||||
|
behaviors: [
|
||||||
|
Behavior.InteractiveChart,
|
||||||
|
Behavior.DrillToDetail,
|
||||||
|
Behavior.DrillBy,
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class CountryMapChartPlugin extends ChartPlugin {
|
export default class CountryMapChartPlugin extends ChartPlugin {
|
||||||
|
|||||||
@@ -19,8 +19,18 @@
|
|||||||
import { ChartProps, getValueFormatter } from '@superset-ui/core';
|
import { ChartProps, getValueFormatter } from '@superset-ui/core';
|
||||||
|
|
||||||
export default function transformProps(chartProps: ChartProps) {
|
export default function transformProps(chartProps: ChartProps) {
|
||||||
const { width, height, formData, queriesData, datasource } = chartProps;
|
|
||||||
const {
|
const {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
formData,
|
||||||
|
queriesData,
|
||||||
|
datasource,
|
||||||
|
hooks = {},
|
||||||
|
filterState,
|
||||||
|
emitCrossFilters,
|
||||||
|
} = chartProps;
|
||||||
|
const {
|
||||||
|
entity,
|
||||||
linearColorScheme,
|
linearColorScheme,
|
||||||
numberFormat,
|
numberFormat,
|
||||||
currencyFormat,
|
currencyFormat,
|
||||||
@@ -49,6 +59,8 @@ export default function transformProps(chartProps: ChartProps) {
|
|||||||
detectedCurrency,
|
detectedCurrency,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { onContextMenu, setDataMask } = hooks;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -59,5 +71,10 @@ export default function transformProps(chartProps: ChartProps) {
|
|||||||
colorScheme,
|
colorScheme,
|
||||||
sliceId,
|
sliceId,
|
||||||
formatter,
|
formatter,
|
||||||
|
entity,
|
||||||
|
onContextMenu,
|
||||||
|
setDataMask,
|
||||||
|
emitCrossFilters,
|
||||||
|
filterState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,10 +133,11 @@ describe('CountryMap (legacy d3)', () => {
|
|||||||
expect(popup!).toHaveStyle({ display: 'none' });
|
expect(popup!).toHaveStyle({ display: 'none' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows tooltip on mouseenter/mousemove/mouseout', async () => {
|
test('emits a cross-filter data mask when a region is clicked', () => {
|
||||||
d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) =>
|
d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) =>
|
||||||
cb(null, mockMapData),
|
cb(null, mockMapData),
|
||||||
);
|
);
|
||||||
|
const setDataMask = jest.fn();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<ReactCountryMap
|
<ReactCountryMap
|
||||||
@@ -147,19 +148,101 @@ describe('CountryMap (legacy d3)', () => {
|
|||||||
linearColorScheme="bnbColors"
|
linearColorScheme="bnbColors"
|
||||||
colorScheme=""
|
colorScheme=""
|
||||||
formatter={jest.fn().mockReturnValue('100')}
|
formatter={jest.fn().mockReturnValue('100')}
|
||||||
|
entity="country_code"
|
||||||
|
emitCrossFilters
|
||||||
|
setDataMask={setDataMask}
|
||||||
|
filterState={{ selectedValues: [] }}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const region = document.querySelector('path.region');
|
const region = document.querySelector('path.region');
|
||||||
expect(region).not.toBeNull();
|
expect(region).not.toBeNull();
|
||||||
|
|
||||||
const popup = document.querySelector('.hover-popup');
|
// A click is only treated as a selection when it follows a mousedown
|
||||||
expect(popup).not.toBeNull();
|
// without dragging beyond the threshold (d3.mouse is mocked to a fixed
|
||||||
|
// position, so the down/up positions match).
|
||||||
|
fireEvent.mouseDown(region!);
|
||||||
|
fireEvent.click(region!);
|
||||||
|
|
||||||
fireEvent.mouseEnter(region!);
|
expect(setDataMask).toHaveBeenCalledTimes(1);
|
||||||
expect(popup!).toHaveStyle({ display: 'block' });
|
expect(setDataMask).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
extraFormData: {
|
||||||
|
filters: [{ col: 'country_code', op: 'IN', val: ['CAN'] }],
|
||||||
|
},
|
||||||
|
filterState: expect.objectContaining({ value: ['CAN'] }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
fireEvent.mouseOut(region!);
|
test('does not emit a cross-filter when emitCrossFilters is disabled', () => {
|
||||||
expect(popup!).toHaveStyle({ display: 'none' });
|
d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) =>
|
||||||
|
cb(null, mockMapData),
|
||||||
|
);
|
||||||
|
const setDataMask = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ReactCountryMap
|
||||||
|
width={500}
|
||||||
|
height={300}
|
||||||
|
data={[{ country_id: 'CAN', metric: 100 }]}
|
||||||
|
country="canada"
|
||||||
|
linearColorScheme="bnbColors"
|
||||||
|
colorScheme=""
|
||||||
|
formatter={jest.fn().mockReturnValue('100')}
|
||||||
|
entity="country_code"
|
||||||
|
emitCrossFilters={false}
|
||||||
|
setDataMask={setDataMask}
|
||||||
|
filterState={{ selectedValues: [] }}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const region = document.querySelector('path.region');
|
||||||
|
fireEvent.mouseDown(region!);
|
||||||
|
fireEvent.click(region!);
|
||||||
|
|
||||||
|
expect(setDataMask).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('opens the context menu with drill-by keyed on the entity control', () => {
|
||||||
|
d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) =>
|
||||||
|
cb(null, mockMapData),
|
||||||
|
);
|
||||||
|
const onContextMenu = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ReactCountryMap
|
||||||
|
width={500}
|
||||||
|
height={300}
|
||||||
|
data={[{ country_id: 'CAN', metric: 100 }]}
|
||||||
|
country="canada"
|
||||||
|
linearColorScheme="bnbColors"
|
||||||
|
colorScheme=""
|
||||||
|
formatter={jest.fn().mockReturnValue('100')}
|
||||||
|
entity="country_code"
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
|
filterState={{ selectedValues: [] }}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const region = document.querySelector('path.region');
|
||||||
|
expect(region).not.toBeNull();
|
||||||
|
|
||||||
|
fireEvent.contextMenu(region!, { clientX: 123, clientY: 45 });
|
||||||
|
|
||||||
|
expect(onContextMenu).toHaveBeenCalledTimes(1);
|
||||||
|
const [[clientX, clientY, payload]] = onContextMenu.mock.calls;
|
||||||
|
expect(clientX).toBe(123);
|
||||||
|
expect(clientY).toBe(45);
|
||||||
|
expect(payload.drillToDetail).toEqual([
|
||||||
|
{ col: 'country_code', op: '==', val: 'CAN', formattedVal: 'CAN' },
|
||||||
|
]);
|
||||||
|
// groupbyFieldName must be the form-data control key ('entity'), not the
|
||||||
|
// selected column value ('country_code'), so DrillByModal can map the
|
||||||
|
// selection back to the chart control.
|
||||||
|
expect(payload.drillBy).toEqual({
|
||||||
|
filters: [{ col: 'country_code', op: '==', val: 'CAN' }],
|
||||||
|
groupbyFieldName: 'entity',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import { ChartProps } from '@superset-ui/core';
|
||||||
|
import transformProps from '../src/transformProps';
|
||||||
|
|
||||||
|
const onContextMenu = jest.fn();
|
||||||
|
const setDataMask = jest.fn();
|
||||||
|
|
||||||
|
const createProps = (formDataOverrides = {}, chartPropsOverrides = {}) =>
|
||||||
|
({
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
formData: {
|
||||||
|
entity: 'country_code',
|
||||||
|
linearColorScheme: 'bnbColors',
|
||||||
|
numberFormat: '.2f',
|
||||||
|
selectCountry: 'France',
|
||||||
|
colorScheme: '',
|
||||||
|
sliceId: 1,
|
||||||
|
metric: 'count',
|
||||||
|
...formDataOverrides,
|
||||||
|
},
|
||||||
|
queriesData: [{ data: [{ country_id: 'FRA', metric: 10 }] }],
|
||||||
|
datasource: { currencyFormats: {}, columnFormats: {} },
|
||||||
|
hooks: { onContextMenu, setDataMask },
|
||||||
|
filterState: { selectedValues: ['FRA'] },
|
||||||
|
emitCrossFilters: true,
|
||||||
|
...chartPropsOverrides,
|
||||||
|
}) as unknown as ChartProps;
|
||||||
|
|
||||||
|
test('forwards cross-filter hooks and state to the chart', () => {
|
||||||
|
const transformed = transformProps(createProps());
|
||||||
|
|
||||||
|
expect(transformed).toMatchObject({
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
entity: 'country_code',
|
||||||
|
onContextMenu,
|
||||||
|
setDataMask,
|
||||||
|
emitCrossFilters: true,
|
||||||
|
filterState: { selectedValues: ['FRA'] },
|
||||||
|
data: [{ country_id: 'FRA', metric: 10 }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lowercases the selected country for map lookup', () => {
|
||||||
|
const transformed = transformProps(createProps());
|
||||||
|
expect(transformed.country).toBe('france');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes a null country when none is selected', () => {
|
||||||
|
const transformed = transformProps(createProps({ selectCountry: undefined }));
|
||||||
|
expect(transformed.country).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('defaults hooks to an empty object when none are provided', () => {
|
||||||
|
const transformed = transformProps(createProps({}, { hooks: undefined }));
|
||||||
|
expect(transformed.onContextMenu).toBeUndefined();
|
||||||
|
expect(transformed.setDataMask).toBeUndefined();
|
||||||
|
});
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
D3_FORMAT_DOCS,
|
D3_FORMAT_DOCS,
|
||||||
D3_FORMAT_OPTIONS,
|
D3_FORMAT_OPTIONS,
|
||||||
D3_TIME_FORMAT_OPTIONS,
|
D3_TIME_FORMAT_OPTIONS,
|
||||||
|
DEFAULT_TIME_FORMAT,
|
||||||
getStandardizedControls,
|
getStandardizedControls,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
import OptionDescription from './OptionDescription';
|
import OptionDescription from './OptionDescription';
|
||||||
@@ -154,7 +155,7 @@ const config: ControlPanelConfig = {
|
|||||||
freeForm: true,
|
freeForm: true,
|
||||||
label: t('Date Time Format'),
|
label: t('Date Time Format'),
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
default: 'smart_date',
|
default: DEFAULT_TIME_FORMAT,
|
||||||
choices: D3_TIME_FORMAT_OPTIONS,
|
choices: D3_TIME_FORMAT_OPTIONS,
|
||||||
description: D3_FORMAT_DOCS,
|
description: D3_FORMAT_DOCS,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
D3_TIME_FORMAT_OPTIONS,
|
D3_TIME_FORMAT_OPTIONS,
|
||||||
sections,
|
sections,
|
||||||
getStandardizedControls,
|
getStandardizedControls,
|
||||||
|
DEFAULT_TIME_FORMAT,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
const config: ControlPanelConfig = {
|
const config: ControlPanelConfig = {
|
||||||
@@ -78,7 +79,7 @@ const config: ControlPanelConfig = {
|
|||||||
freeForm: true,
|
freeForm: true,
|
||||||
label: t('Date Time Format'),
|
label: t('Date Time Format'),
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
default: 'smart_date',
|
default: DEFAULT_TIME_FORMAT,
|
||||||
choices: D3_TIME_FORMAT_OPTIONS,
|
choices: D3_TIME_FORMAT_OPTIONS,
|
||||||
description: D3_FORMAT_DOCS,
|
description: D3_FORMAT_DOCS,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ function WorldMap(element: HTMLElement, props: WorldMapProps): void {
|
|||||||
processedData = filteredData.map(d => ({
|
processedData = filteredData.map(d => ({
|
||||||
...d,
|
...d,
|
||||||
radius: radiusScale(Math.sqrt(d.m2)),
|
radius: radiusScale(Math.sqrt(d.m2)),
|
||||||
fillColor: d.m1 != null ? colorFn(d.m1) ?? theme.colorBorder : theme.colorBorder,
|
fillColor:
|
||||||
|
d.m1 != null ? (colorFn(d.m1) ?? theme.colorBorder) : theme.colorBorder,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
D3_TIME_FORMAT_OPTIONS,
|
D3_TIME_FORMAT_OPTIONS,
|
||||||
D3_FORMAT_DOCS,
|
D3_FORMAT_DOCS,
|
||||||
D3_FORMAT_OPTIONS,
|
D3_FORMAT_OPTIONS,
|
||||||
|
DEFAULT_TIME_FORMAT,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -235,7 +236,7 @@ export const xAxisFormat: CustomControlItem = {
|
|||||||
label: t('X Axis Format'),
|
label: t('X Axis Format'),
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
choices: D3_TIME_FORMAT_OPTIONS,
|
choices: D3_TIME_FORMAT_OPTIONS,
|
||||||
default: 'smart_date',
|
default: DEFAULT_TIME_FORMAT,
|
||||||
description: D3_FORMAT_DOCS,
|
description: D3_FORMAT_DOCS,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
export default function buildQuery(formData: QueryFormData) {
|
export default function buildQuery(formData: QueryFormData) {
|
||||||
const { cols: groupby } = formData;
|
const { cols: groupby, extra_form_data } = formData;
|
||||||
|
|
||||||
const queryContextA = buildQueryContext(formData, baseQueryObject => {
|
const queryContextA = buildQueryContext(formData, baseQueryObject => {
|
||||||
const postProcessing: PostProcessingRule[] = [];
|
const postProcessing: PostProcessingRule[] = [];
|
||||||
@@ -58,14 +58,24 @@ export default function buildQuery(formData: QueryFormData) {
|
|||||||
timeOffsets = timeOffsets.concat(['inherit']);
|
timeOffsets = timeOffsets.concat(['inherit']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
extra_form_data?.time_compare &&
|
||||||
|
!timeOffsets.includes(extra_form_data.time_compare)
|
||||||
|
) {
|
||||||
|
timeOffsets = [extra_form_data.time_compare];
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
...baseQueryObject,
|
...baseQueryObject,
|
||||||
groupby,
|
groupby,
|
||||||
post_processing: postProcessing,
|
post_processing: postProcessing,
|
||||||
time_offsets: isTimeComparison(formData, baseQueryObject)
|
time_offsets:
|
||||||
? ensureIsArray(timeOffsets)
|
isTimeComparison(formData, baseQueryObject) ||
|
||||||
: [],
|
extra_form_data?.time_compare
|
||||||
|
? ensureIsArray(timeOffsets)
|
||||||
|
: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -111,7 +111,11 @@ export default function transformProps(chartProps: ChartProps) {
|
|||||||
const metrics = chartProps.datasource?.metrics || [];
|
const metrics = chartProps.datasource?.metrics || [];
|
||||||
const originalLabel = getOriginalLabel(metric, metrics);
|
const originalLabel = getOriginalLabel(metric, metrics);
|
||||||
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
||||||
const timeComparison = ensureIsArray(chartProps.rawFormData?.time_compare)[0];
|
|
||||||
|
const dashboardTimeCompare = formData?.extraFormData?.time_compare;
|
||||||
|
const timeComparison =
|
||||||
|
dashboardTimeCompare ||
|
||||||
|
ensureIsArray(chartProps.rawFormData?.time_compare)[0];
|
||||||
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
||||||
const currentTimeRangeFilter = chartProps.rawFormData?.adhoc_filters?.filter(
|
const currentTimeRangeFilter = chartProps.rawFormData?.adhoc_filters?.filter(
|
||||||
(adhoc_filter: SimpleAdhocFilter) =>
|
(adhoc_filter: SimpleAdhocFilter) =>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
ControlPanelState,
|
ControlPanelState,
|
||||||
getTemporalColumns,
|
getTemporalColumns,
|
||||||
sharedControls,
|
sharedControls,
|
||||||
|
DEFAULT_TIME_FORMAT,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
const config: ControlPanelConfig = {
|
const config: ControlPanelConfig = {
|
||||||
@@ -153,12 +154,26 @@ const config: ControlPanelConfig = {
|
|||||||
label: t('Date format'),
|
label: t('Date format'),
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
choices: D3_TIME_FORMAT_OPTIONS,
|
choices: D3_TIME_FORMAT_OPTIONS,
|
||||||
default: 'smart_date',
|
default: DEFAULT_TIME_FORMAT,
|
||||||
description: D3_FORMAT_DOCS,
|
description: D3_FORMAT_DOCS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
['zoomable'],
|
['zoomable'],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'y_axis_slider',
|
||||||
|
config: {
|
||||||
|
type: 'CheckboxControl',
|
||||||
|
label: t('Y-axis range slider'),
|
||||||
|
default: false,
|
||||||
|
renderTrigger: true,
|
||||||
|
description: t(
|
||||||
|
'Show a draggable slider to control the visible range of the Y-axis.',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export default function transformProps(
|
|||||||
yAxisTitlePosition,
|
yAxisTitlePosition,
|
||||||
sliceId,
|
sliceId,
|
||||||
zoomable,
|
zoomable,
|
||||||
|
yAxisSlider,
|
||||||
} = formData as BoxPlotQueryFormData;
|
} = formData as BoxPlotQueryFormData;
|
||||||
const refs: Refs = {};
|
const refs: Refs = {};
|
||||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||||
@@ -257,6 +258,28 @@ export default function transformProps(
|
|||||||
convertInteger(yAxisTitleMargin),
|
convertInteger(yAxisTitleMargin),
|
||||||
convertInteger(xAxisTitleMargin),
|
convertInteger(xAxisTitleMargin),
|
||||||
);
|
);
|
||||||
|
const dataZoom = [
|
||||||
|
...(zoomable
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
zoomOnMouseWheel: false,
|
||||||
|
moveOnMouseWheel: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(yAxisSlider
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: 'slider',
|
||||||
|
show: true,
|
||||||
|
yAxisIndex: [0],
|
||||||
|
// Adjust the axis window without dropping data points outside the range.
|
||||||
|
filterMode: 'none',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
const echartOptions: EChartsCoreOption = {
|
const echartOptions: EChartsCoreOption = {
|
||||||
grid: {
|
grid: {
|
||||||
...defaultGrid,
|
...defaultGrid,
|
||||||
@@ -298,15 +321,7 @@ export default function transformProps(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dataZoom: zoomable
|
dataZoom,
|
||||||
? [
|
|
||||||
{
|
|
||||||
type: 'inside',
|
|
||||||
zoomOnMouseWheel: false,
|
|
||||||
moveOnMouseWheel: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type BoxPlotQueryFormData = QueryFormData & {
|
|||||||
numberFormat?: string;
|
numberFormat?: string;
|
||||||
whiskerOptions?: BoxPlotFormDataWhiskerOptions;
|
whiskerOptions?: BoxPlotFormDataWhiskerOptions;
|
||||||
xTickLayout?: BoxPlotFormXTickLayout;
|
xTickLayout?: BoxPlotFormXTickLayout;
|
||||||
|
yAxisSlider?: boolean;
|
||||||
} & TitleFormData;
|
} & TitleFormData;
|
||||||
|
|
||||||
export type BoxPlotFormDataWhiskerOptions =
|
export type BoxPlotFormDataWhiskerOptions =
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
D3_TIME_FORMAT_OPTIONS,
|
D3_TIME_FORMAT_OPTIONS,
|
||||||
getStandardizedControls,
|
getStandardizedControls,
|
||||||
sharedControls,
|
sharedControls,
|
||||||
|
DEFAULT_TIME_FORMAT,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
import { DEFAULT_FORM_DATA } from './types';
|
import { DEFAULT_FORM_DATA } from './types';
|
||||||
import { legendSection } from '../controls';
|
import { legendSection } from '../controls';
|
||||||
@@ -188,7 +189,7 @@ const config: ControlPanelConfig = {
|
|||||||
label: t('Date format'),
|
label: t('Date format'),
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
choices: D3_TIME_FORMAT_OPTIONS,
|
choices: D3_TIME_FORMAT_OPTIONS,
|
||||||
default: 'smart_date',
|
default: DEFAULT_TIME_FORMAT,
|
||||||
description: D3_FORMAT_DOCS,
|
description: D3_FORMAT_DOCS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
sharedControls,
|
sharedControls,
|
||||||
ControlFormItemSpec,
|
ControlFormItemSpec,
|
||||||
getStandardizedControls,
|
getStandardizedControls,
|
||||||
|
DEFAULT_TIME_FORMAT,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
import { DEFAULT_FORM_DATA } from './types';
|
import { DEFAULT_FORM_DATA } from './types';
|
||||||
import { LabelPositionEnum } from '../types';
|
import { LabelPositionEnum } from '../types';
|
||||||
@@ -181,7 +182,7 @@ const config: ControlPanelConfig = {
|
|||||||
label: t('Date format'),
|
label: t('Date format'),
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
choices: D3_TIME_FORMAT_OPTIONS,
|
choices: D3_TIME_FORMAT_OPTIONS,
|
||||||
default: 'smart_date',
|
default: DEFAULT_TIME_FORMAT,
|
||||||
description: D3_FORMAT_DOCS,
|
description: D3_FORMAT_DOCS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
D3_FORMAT_OPTIONS,
|
D3_FORMAT_OPTIONS,
|
||||||
D3_TIME_FORMAT_OPTIONS,
|
D3_TIME_FORMAT_OPTIONS,
|
||||||
getStandardizedControls,
|
getStandardizedControls,
|
||||||
|
DEFAULT_TIME_FORMAT,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
import { DEFAULT_FORM_DATA } from './types';
|
import { DEFAULT_FORM_DATA } from './types';
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ const config: ControlPanelConfig = {
|
|||||||
label: t('Date format'),
|
label: t('Date format'),
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
choices: D3_TIME_FORMAT_OPTIONS,
|
choices: D3_TIME_FORMAT_OPTIONS,
|
||||||
default: 'smart_date',
|
default: DEFAULT_TIME_FORMAT,
|
||||||
description: D3_FORMAT_DOCS,
|
description: D3_FORMAT_DOCS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -182,7 +182,6 @@ const config: ControlPanelConfig = {
|
|||||||
name: 'x_axis_time_format',
|
name: 'x_axis_time_format',
|
||||||
config: {
|
config: {
|
||||||
...sharedControls.x_axis_time_format,
|
...sharedControls.x_axis_time_format,
|
||||||
default: 'smart_date',
|
|
||||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
checkColumnType(
|
checkColumnType(
|
||||||
|
|||||||
@@ -396,3 +396,102 @@ test('does not emit cross-filter when no dimensions and time-based X-axis', asyn
|
|||||||
expect(setDataMaskMock).not.toHaveBeenCalled();
|
expect(setDataMaskMock).not.toHaveBeenCalled();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test for issue #41102: horizontal bar cross-filter must use the category
|
||||||
|
// value, not the metric. For horizontal bars the data tuple is value-first
|
||||||
|
// (e.g. [100, 'Product A']), so relying on data[0] emitted the metric value.
|
||||||
|
test('emits cross-filter on the category value for a horizontal categorical bar', async () => {
|
||||||
|
const setDataMaskMock = jest.fn();
|
||||||
|
|
||||||
|
const propsWithHorizontalXAxis: TimeseriesChartTransformedProps = {
|
||||||
|
...defaultProps,
|
||||||
|
emitCrossFilters: true,
|
||||||
|
setDataMask: setDataMaskMock,
|
||||||
|
groupby: [], // No dimensions
|
||||||
|
xAxis: {
|
||||||
|
label: 'category_column',
|
||||||
|
type: AxisType.Category, // Categorical X-axis
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<EchartsTimeseries {...propsWithHorizontalXAxis} />);
|
||||||
|
|
||||||
|
const lastCall = mockEchart.mock.calls.at(-1);
|
||||||
|
expect(lastCall).toBeDefined();
|
||||||
|
const [props] = lastCall as [EchartsProps];
|
||||||
|
|
||||||
|
const clickHandler = props.eventHandlers?.click;
|
||||||
|
if (clickHandler) {
|
||||||
|
clickHandler({
|
||||||
|
seriesName: 'Sales', // This is the metric name
|
||||||
|
data: [100, 'Product A'], // Horizontal: value first, category second
|
||||||
|
name: 'Product A',
|
||||||
|
dataIndex: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(setDataMaskMock).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
{ timeout: 500 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Must filter on the category ('Product A'), not the metric value (100)
|
||||||
|
const dataMaskCall = setDataMaskMock.mock.calls[0][0];
|
||||||
|
expect(dataMaskCall.extraFormData.filters).toEqual([
|
||||||
|
{
|
||||||
|
col: 'category_column',
|
||||||
|
op: 'IN',
|
||||||
|
val: ['Product A'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test for issue #41102: the context-menu ("Add cross-filter") path must also
|
||||||
|
// use the category value, not the metric, for a horizontal categorical bar.
|
||||||
|
test('context menu cross-filter uses the category value for a horizontal categorical bar', async () => {
|
||||||
|
const onContextMenuMock = jest.fn();
|
||||||
|
|
||||||
|
const propsWithHorizontalXAxis: TimeseriesChartTransformedProps = {
|
||||||
|
...defaultProps,
|
||||||
|
emitCrossFilters: true,
|
||||||
|
onContextMenu: onContextMenuMock,
|
||||||
|
groupby: [], // No dimensions
|
||||||
|
xAxis: {
|
||||||
|
label: 'category_column',
|
||||||
|
type: AxisType.Category, // Categorical X-axis
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<EchartsTimeseries {...propsWithHorizontalXAxis} />);
|
||||||
|
|
||||||
|
const lastCall = mockEchart.mock.calls.at(-1);
|
||||||
|
expect(lastCall).toBeDefined();
|
||||||
|
const [props] = lastCall as [EchartsProps];
|
||||||
|
|
||||||
|
const contextMenuHandler = props.eventHandlers?.contextmenu;
|
||||||
|
expect(contextMenuHandler).toBeDefined();
|
||||||
|
if (contextMenuHandler) {
|
||||||
|
await contextMenuHandler({
|
||||||
|
seriesName: 'Sales', // This is the metric name
|
||||||
|
data: [100, 'Product A'], // Horizontal: value first, category second
|
||||||
|
name: 'Product A',
|
||||||
|
event: { stop: jest.fn(), event: { clientX: 10, clientY: 20 } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onContextMenuMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The cross-filter must use the category ('Product A'), not the metric (100)
|
||||||
|
const { crossFilter } = onContextMenuMock.mock.calls[0][2];
|
||||||
|
expect(crossFilter.dataMask.extraFormData.filters).toEqual([
|
||||||
|
{
|
||||||
|
col: 'category_column',
|
||||||
|
op: 'IN',
|
||||||
|
val: ['Product A'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -234,9 +234,12 @@ export default function EchartsTimeseries({
|
|||||||
// Cross-filter by dimension (original behavior)
|
// Cross-filter by dimension (original behavior)
|
||||||
const { seriesName: name } = props;
|
const { seriesName: name } = props;
|
||||||
handleChange(name);
|
handleChange(name);
|
||||||
} else if (canCrossFilterByXAxis && props.data?.[0] != null) {
|
} else if (canCrossFilterByXAxis && props.name != null) {
|
||||||
// Cross-filter by X-axis value when no dimensions (issue #25334)
|
// Cross-filter by X-axis value when no dimensions (issue #25334).
|
||||||
handleXAxisChange(props.data[0]);
|
// Use `name` (the category-axis value) instead of `data[0]`: for
|
||||||
|
// horizontal bars the data tuple is value-first, so `data[0]` would
|
||||||
|
// be the metric value rather than the category (issue #41102).
|
||||||
|
handleXAxisChange(props.name);
|
||||||
}
|
}
|
||||||
}, TIMER_DURATION);
|
}, TIMER_DURATION);
|
||||||
},
|
},
|
||||||
@@ -318,8 +321,10 @@ export default function EchartsTimeseries({
|
|||||||
let crossFilter;
|
let crossFilter;
|
||||||
if (hasDimensions) {
|
if (hasDimensions) {
|
||||||
crossFilter = getCrossFilterDataMask(seriesName);
|
crossFilter = getCrossFilterDataMask(seriesName);
|
||||||
} else if (canCrossFilterByXAxis && data?.[0] != null) {
|
} else if (canCrossFilterByXAxis && eventParams.name != null) {
|
||||||
crossFilter = getXAxisCrossFilterDataMask(data[0]);
|
// Use `name` (the category-axis value), not `data[0]`, so horizontal
|
||||||
|
// bars cross-filter on the category and not the metric (issue #41102).
|
||||||
|
crossFilter = getXAxisCrossFilterDataMask(eventParams.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||||
|
|||||||
@@ -174,7 +174,6 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
|||||||
name: 'x_axis_time_format',
|
name: 'x_axis_time_format',
|
||||||
config: {
|
config: {
|
||||||
...sharedControls.x_axis_time_format,
|
...sharedControls.x_axis_time_format,
|
||||||
default: 'smart_date',
|
|
||||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
(isXAxis ? isVertical(controls) : isHorizontal(controls)) &&
|
(isXAxis ? isVertical(controls) : isHorizontal(controls)) &&
|
||||||
|
|||||||
@@ -147,7 +147,6 @@ const config: ControlPanelConfig = {
|
|||||||
name: 'x_axis_time_format',
|
name: 'x_axis_time_format',
|
||||||
config: {
|
config: {
|
||||||
...sharedControls.x_axis_time_format,
|
...sharedControls.x_axis_time_format,
|
||||||
default: 'smart_date',
|
|
||||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
checkColumnType(
|
checkColumnType(
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ const config: ControlPanelConfig = {
|
|||||||
name: 'x_axis_time_format',
|
name: 'x_axis_time_format',
|
||||||
config: {
|
config: {
|
||||||
...sharedControls.x_axis_time_format,
|
...sharedControls.x_axis_time_format,
|
||||||
default: 'smart_date',
|
|
||||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
checkColumnType(
|
checkColumnType(
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ const config: ControlPanelConfig = {
|
|||||||
name: 'x_axis_time_format',
|
name: 'x_axis_time_format',
|
||||||
config: {
|
config: {
|
||||||
...sharedControls.x_axis_time_format,
|
...sharedControls.x_axis_time_format,
|
||||||
default: 'smart_date',
|
|
||||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
checkColumnType(
|
checkColumnType(
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ const config: ControlPanelConfig = {
|
|||||||
name: 'x_axis_time_format',
|
name: 'x_axis_time_format',
|
||||||
config: {
|
config: {
|
||||||
...sharedControls.x_axis_time_format,
|
...sharedControls.x_axis_time_format,
|
||||||
default: 'smart_date',
|
|
||||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
checkColumnType(
|
checkColumnType(
|
||||||
|
|||||||
@@ -258,7 +258,6 @@ export const tooltipTimeFormatControl: ControlSetItem = {
|
|||||||
config: {
|
config: {
|
||||||
...sharedControls.x_axis_time_format,
|
...sharedControls.x_axis_time_format,
|
||||||
label: t('Tooltip time format'),
|
label: t('Tooltip time format'),
|
||||||
default: 'smart_date',
|
|
||||||
clearable: false,
|
clearable: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import { QueryFormData } from '@superset-ui/core';
|
||||||
|
import buildQuery from '../../../src/BigNumber/BigNumberPeriodOverPeriod/buildQuery';
|
||||||
|
|
||||||
|
describe('BigNumberPeriodOverPeriod buildQuery', () => {
|
||||||
|
const baseFormData: QueryFormData = {
|
||||||
|
datasource: '1__table',
|
||||||
|
viz_type: 'pop_kpi',
|
||||||
|
metric: 'count',
|
||||||
|
cols: [],
|
||||||
|
adhoc_filters: [
|
||||||
|
{
|
||||||
|
clause: 'WHERE',
|
||||||
|
subject: 'order_date',
|
||||||
|
operator: 'TEMPORAL_RANGE',
|
||||||
|
comparator: '2003-07-01 : 2004-01-01',
|
||||||
|
expressionType: 'SIMPLE',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test('flows extra_form_data.time_compare override into time_offsets', () => {
|
||||||
|
const queryContext = buildQuery({
|
||||||
|
...baseFormData,
|
||||||
|
extra_form_data: { time_compare: '1 year ago' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryContext.queries[0].time_offsets).toEqual(['1 year ago']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requests offsets from the override even without the chart time_compare control', () => {
|
||||||
|
const queryContext = buildQuery({
|
||||||
|
...baseFormData,
|
||||||
|
time_compare: undefined,
|
||||||
|
extra_form_data: { time_compare: '1 year ago' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryContext.queries[0].time_offsets).toEqual(['1 year ago']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not duplicate the offset when it already matches time_compare', () => {
|
||||||
|
const queryContext = buildQuery({
|
||||||
|
...baseFormData,
|
||||||
|
time_compare: ['1 year ago'],
|
||||||
|
extra_form_data: { time_compare: '1 year ago' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryContext.queries[0].time_offsets).toEqual(['1 year ago']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('omits time_offsets when neither the control nor the override is set', () => {
|
||||||
|
const queryContext = buildQuery(baseFormData);
|
||||||
|
|
||||||
|
expect(queryContext.queries[0].time_offsets).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -71,6 +71,15 @@ describe('BoxPlot transformProps', () => {
|
|||||||
theme: supersetTheme,
|
theme: supersetTheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buildChartProps = (formDataOverrides: Partial<SqlaFormData> = {}) =>
|
||||||
|
new ChartProps({
|
||||||
|
formData: { ...formData, ...formDataOverrides },
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
queriesData: chartProps.queriesData,
|
||||||
|
theme: supersetTheme,
|
||||||
|
}) as EchartsBoxPlotChartProps;
|
||||||
|
|
||||||
test('should transform chart props for viz', () => {
|
test('should transform chart props for viz', () => {
|
||||||
expect(transformProps(chartProps as EchartsBoxPlotChartProps)).toEqual(
|
expect(transformProps(chartProps as EchartsBoxPlotChartProps)).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -125,4 +134,41 @@ describe('BoxPlot transformProps', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should add a vertical Y-axis slider to dataZoom when yAxisSlider is enabled', () => {
|
||||||
|
const { echartOptions } = transformProps(
|
||||||
|
buildChartProps({ yAxisSlider: true }),
|
||||||
|
);
|
||||||
|
expect((echartOptions as any).dataZoom).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'slider',
|
||||||
|
show: true,
|
||||||
|
yAxisIndex: [0],
|
||||||
|
filterMode: 'none',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not add a Y-axis slider when yAxisSlider is disabled', () => {
|
||||||
|
const { echartOptions } = transformProps(
|
||||||
|
buildChartProps({ yAxisSlider: false }),
|
||||||
|
);
|
||||||
|
expect((echartOptions as any).dataZoom).not.toContainEqual(
|
||||||
|
expect.objectContaining({ type: 'slider' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should combine zoomable and yAxisSlider dataZoom entries', () => {
|
||||||
|
const { echartOptions } = transformProps(
|
||||||
|
buildChartProps({ zoomable: true, yAxisSlider: true }),
|
||||||
|
);
|
||||||
|
expect((echartOptions as any).dataZoom).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ type: 'inside' }),
|
||||||
|
expect.objectContaining({ type: 'slider', yAxisIndex: [0] }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function getQueryMode(formData: TableChartFormData) {
|
|||||||
return hasRawColumns ? QueryMode.Raw : QueryMode.Aggregate;
|
return hasRawColumns ? QueryMode.Raw : QueryMode.Aggregate;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildQuery: BuildQuery<TableChartFormData> = (
|
export const buildQuery: BuildQuery<TableChartFormData> = (
|
||||||
formData: TableChartFormData,
|
formData: TableChartFormData,
|
||||||
options,
|
options,
|
||||||
) => {
|
) => {
|
||||||
@@ -217,6 +217,17 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
|||||||
|
|
||||||
const moreProps: Partial<QueryObject> = {};
|
const moreProps: Partial<QueryObject> = {};
|
||||||
const ownState = options?.ownState ?? {};
|
const ownState = options?.ownState ?? {};
|
||||||
|
// Server pagination sizing, shared between the per-page request below and
|
||||||
|
// the filter-change reset further down.
|
||||||
|
const pageSize =
|
||||||
|
Number(ownState.pageSize ?? formDataCopy.server_page_length) || 0;
|
||||||
|
const configuredRowLimit = Number(formDataCopy.row_limit) || 0;
|
||||||
|
// row_limit for the first page, capped by the configured row limit. Used
|
||||||
|
// when a filter change resets pagination back to page 0.
|
||||||
|
const firstPageRowLimit =
|
||||||
|
configuredRowLimit > 0
|
||||||
|
? Math.min(pageSize, configuredRowLimit)
|
||||||
|
: pageSize;
|
||||||
// Build Query flag to check if its for either download as csv, excel or json
|
// Build Query flag to check if its for either download as csv, excel or json
|
||||||
const isDownloadQuery =
|
const isDownloadQuery =
|
||||||
['csv', 'xlsx'].includes(formData?.result_format || '') ||
|
['csv', 'xlsx'].includes(formData?.result_format || '') ||
|
||||||
@@ -229,11 +240,24 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isDownloadQuery && formDataCopy.server_pagination) {
|
if (!isDownloadQuery && formDataCopy.server_pagination) {
|
||||||
const pageSize = ownState.pageSize ?? formDataCopy.server_page_length;
|
// Never page past the configured row limit. Clamping the page to the last
|
||||||
const currentPage = ownState.currentPage ?? 0;
|
// one that still falls within the limit keeps the request inside the cap
|
||||||
|
// and avoids emitting row_limit: 0, which the backend treats as
|
||||||
|
// "no limit" rather than "no rows" (see helpers.py get_sqla_query).
|
||||||
|
const lastPage =
|
||||||
|
configuredRowLimit > 0 && pageSize > 0
|
||||||
|
? Math.max(Math.ceil(configuredRowLimit / pageSize) - 1, 0)
|
||||||
|
: Number(ownState.currentPage) || 0;
|
||||||
|
const currentPage = Math.min(Number(ownState.currentPage) || 0, lastPage);
|
||||||
|
const rowOffset = currentPage * pageSize;
|
||||||
|
const remainingRows =
|
||||||
|
configuredRowLimit > 0
|
||||||
|
? Math.max(configuredRowLimit - rowOffset, 0)
|
||||||
|
: pageSize;
|
||||||
|
|
||||||
moreProps.row_limit = pageSize;
|
moreProps.row_limit =
|
||||||
moreProps.row_offset = currentPage * pageSize;
|
configuredRowLimit > 0 ? Math.min(pageSize, remainingRows) : pageSize;
|
||||||
|
moreProps.row_offset = rowOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// getting sort by in case of server pagination from own state
|
// getting sort by in case of server pagination from own state
|
||||||
@@ -263,11 +287,19 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
|||||||
JSON.stringify(options?.extras?.cachedChanges?.[formData.slice_id]) !==
|
JSON.stringify(options?.extras?.cachedChanges?.[formData.slice_id]) !==
|
||||||
JSON.stringify(queryObject.filters)
|
JSON.stringify(queryObject.filters)
|
||||||
) {
|
) {
|
||||||
queryObject = { ...queryObject, row_offset: 0 };
|
// Reset to the first page: restore the full first-page row_limit rather
|
||||||
|
// than carrying over the last page's capped value.
|
||||||
|
queryObject = {
|
||||||
|
...queryObject,
|
||||||
|
row_offset: 0,
|
||||||
|
row_limit: firstPageRowLimit,
|
||||||
|
};
|
||||||
const modifiedOwnState = {
|
const modifiedOwnState = {
|
||||||
...options?.ownState,
|
...options?.ownState,
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
pageSize: queryObject.row_limit ?? 0,
|
// Persist the user-selected page size, not the per-request row_limit,
|
||||||
|
// which may be capped to the remaining rows on the last page.
|
||||||
|
pageSize,
|
||||||
};
|
};
|
||||||
updateTableOwnState(options?.hooks?.setDataMask, modifiedOwnState);
|
updateTableOwnState(options?.hooks?.setDataMask, modifiedOwnState);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { QueryMode, TimeGranularity, VizType } from '@superset-ui/core';
|
import { QueryMode, TimeGranularity, VizType } from '@superset-ui/core';
|
||||||
import buildQuery from '../src/buildQuery';
|
import buildQuery, {
|
||||||
|
buildQuery as buildQueryUncached,
|
||||||
|
} from '../src/buildQuery';
|
||||||
import { TableChartFormData } from '../src/types';
|
import { TableChartFormData } from '../src/types';
|
||||||
|
|
||||||
const basicFormData: TableChartFormData = {
|
const basicFormData: TableChartFormData = {
|
||||||
@@ -278,6 +280,172 @@ describe('plugin-chart-table', () => {
|
|||||||
|
|
||||||
expect(queries[0].filters?.some(f => f.op === 'ILIKE')).toBeFalsy();
|
expect(queries[0].filters?.some(f => f.op === 'ILIKE')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('uses user row limit when it is lower than server page size', () => {
|
||||||
|
const { queries } = buildQuery(
|
||||||
|
{
|
||||||
|
...baseFormDataWithServerPagination,
|
||||||
|
row_limit: 10,
|
||||||
|
server_page_length: 20,
|
||||||
|
slice_id: 101,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ownState: {
|
||||||
|
currentPage: 0,
|
||||||
|
pageSize: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queries[0]).toMatchObject({
|
||||||
|
row_limit: 10,
|
||||||
|
row_offset: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('limits server page size by remaining rows inside user row limit', () => {
|
||||||
|
const { queries } = buildQuery(
|
||||||
|
{
|
||||||
|
...baseFormDataWithServerPagination,
|
||||||
|
row_limit: 120,
|
||||||
|
server_page_length: 50,
|
||||||
|
slice_id: 102,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ownState: {
|
||||||
|
currentPage: 2,
|
||||||
|
pageSize: 50,
|
||||||
|
sortBy: [{ key: 'category', desc: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queries[0]).toMatchObject({
|
||||||
|
orderby: [['category', false]],
|
||||||
|
row_limit: 20,
|
||||||
|
row_offset: 100,
|
||||||
|
});
|
||||||
|
expect(queries[1]).toMatchObject({
|
||||||
|
is_rowcount: true,
|
||||||
|
row_limit: 120,
|
||||||
|
row_offset: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clamps pages beyond the row limit instead of emitting row_limit: 0', () => {
|
||||||
|
const { queries } = buildQuery(
|
||||||
|
{
|
||||||
|
...baseFormDataWithServerPagination,
|
||||||
|
row_limit: 120,
|
||||||
|
server_page_length: 50,
|
||||||
|
slice_id: 103,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ownState: {
|
||||||
|
// Page 5 is well past the cap; offset would be 250 > 120, which
|
||||||
|
// previously made row_limit collapse to 0 ("no limit").
|
||||||
|
currentPage: 5,
|
||||||
|
pageSize: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queries[0].row_limit).not.toBe(0);
|
||||||
|
expect(queries[0]).toMatchObject({
|
||||||
|
row_limit: 20,
|
||||||
|
row_offset: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restores the full first-page row limit after a filter change reset', () => {
|
||||||
|
// Uncached export lets us seed cachedChanges directly; the default
|
||||||
|
// export overrides extras with its own closure.
|
||||||
|
const { queries } = buildQueryUncached(
|
||||||
|
{
|
||||||
|
...baseFormDataWithServerPagination,
|
||||||
|
row_limit: 120,
|
||||||
|
server_page_length: 50,
|
||||||
|
slice_id: 104,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// User was on the capped last page (row_limit would be 20)...
|
||||||
|
ownState: {
|
||||||
|
currentPage: 2,
|
||||||
|
pageSize: 50,
|
||||||
|
},
|
||||||
|
// ...then an external filter changed, so the cached filters differ
|
||||||
|
// from the current ones and pagination resets to page 0.
|
||||||
|
extras: {
|
||||||
|
cachedChanges: {
|
||||||
|
104: [{ col: 'category', op: '==', val: 'previous' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queries[0].row_limit).not.toBe(0);
|
||||||
|
expect(queries[0]).toMatchObject({
|
||||||
|
row_limit: 50,
|
||||||
|
row_offset: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('persists the user page size, not the capped limit, on filter reset', () => {
|
||||||
|
const setDataMask = jest.fn();
|
||||||
|
buildQueryUncached(
|
||||||
|
{
|
||||||
|
...baseFormDataWithServerPagination,
|
||||||
|
row_limit: 120,
|
||||||
|
server_page_length: 50,
|
||||||
|
slice_id: 106,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// On the capped last page, the per-request row_limit is 20.
|
||||||
|
ownState: {
|
||||||
|
currentPage: 2,
|
||||||
|
pageSize: 50,
|
||||||
|
},
|
||||||
|
extras: {
|
||||||
|
cachedChanges: {
|
||||||
|
106: [{ col: 'category', op: '==', val: 'previous' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hooks: { setDataMask, setCachedChanges: jest.fn() },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// The persisted page size must stay 50, not collapse to the capped 20.
|
||||||
|
expect(setDataMask).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
ownState: expect.objectContaining({
|
||||||
|
currentPage: 0,
|
||||||
|
pageSize: 50,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to the page size when no row limit is configured', () => {
|
||||||
|
const { queries } = buildQuery(
|
||||||
|
{
|
||||||
|
...baseFormDataWithServerPagination,
|
||||||
|
row_limit: undefined,
|
||||||
|
server_page_length: 50,
|
||||||
|
slice_id: 105,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ownState: {
|
||||||
|
currentPage: 3,
|
||||||
|
pageSize: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queries[0]).toMatchObject({
|
||||||
|
row_limit: 50,
|
||||||
|
row_offset: 150,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ jest.mock('@superset-ui/core/components/Select/AsyncSelect', () => () => (
|
|||||||
<div data-test="mock-async-select" />
|
<div data-test="mock-async-select" />
|
||||||
));
|
));
|
||||||
jest.mock('src/core/editors', () => ({
|
jest.mock('src/core/editors', () => ({
|
||||||
EditorHost: ({ value }: { value: string }) => (
|
EditorHost: ({ value, height }: { value: string; height: string }) => (
|
||||||
<div data-test="mock-async-ace-editor">{value}</div>
|
<div data-test="mock-async-ace-editor" data-height={height}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -79,6 +81,18 @@ describe('TemplateParamsEditor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('renders the editor with a bounded height to avoid overflowing the popover', async () => {
|
||||||
|
const { container, getByTestId } = setup();
|
||||||
|
fireEvent.click(getByText(container, 'Parameters'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(getByTestId('mock-async-ace-editor')).toHaveAttribute(
|
||||||
|
'data-height',
|
||||||
|
'360px',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('renders templateParams', async () => {
|
test('renders templateParams', async () => {
|
||||||
const { container, getByTestId } = setup();
|
const { container, getByTestId } = setup();
|
||||||
fireEvent.click(getByText(container, 'Parameters'));
|
fireEvent.click(getByText(container, 'Parameters'));
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ import {
|
|||||||
import { EditorHost } from 'src/core/editors';
|
import { EditorHost } from 'src/core/editors';
|
||||||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||||
|
|
||||||
const StyledEditorHost = styled(EditorHost)`
|
const EditorOutline = styled.div`
|
||||||
&.ace_editor {
|
border: 1px solid ${({ theme }) => theme.colorBorder};
|
||||||
border: 1px solid ${({ theme }) => theme.colorBorder};
|
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledParagraph = styled.p`
|
const StyledParagraph = styled.p`
|
||||||
@@ -87,14 +86,16 @@ const TemplateParamsEditor = ({
|
|||||||
</a>{' '}
|
</a>{' '}
|
||||||
{t('syntax.')}
|
{t('syntax.')}
|
||||||
</StyledParagraph>
|
</StyledParagraph>
|
||||||
<StyledEditorHost
|
<EditorOutline>
|
||||||
id={`template-params-${queryEditorId}`}
|
<EditorHost
|
||||||
height="800px"
|
id={`template-params-${queryEditorId}`}
|
||||||
onChange={debounce(onChange, Constants.FAST_DEBOUNCE)}
|
height="360px"
|
||||||
language={language === 'yaml' ? 'yaml' : 'json'}
|
onChange={debounce(onChange, Constants.FAST_DEBOUNCE)}
|
||||||
width="100%"
|
language={language === 'yaml' ? 'yaml' : 'json'}
|
||||||
value={code}
|
width="100%"
|
||||||
/>
|
value={code}
|
||||||
|
/>
|
||||||
|
</EditorOutline>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -200,6 +200,27 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.unsavedQueryEditor.sql).toBe(sql);
|
expect(newState.unsavedQueryEditor.sql).toBe(sql);
|
||||||
expect(newState.unsavedQueryEditor.id).toBe(qe!.id);
|
expect(newState.unsavedQueryEditor.id).toBe(qe!.id);
|
||||||
});
|
});
|
||||||
|
test('should set Sql when dispatched with tabViewId (backend persistence)', () => {
|
||||||
|
// Simulate SqllabBackendPersistence: queryEditor gets a tabViewId after save
|
||||||
|
const tabViewId = 'tab-view-42';
|
||||||
|
const migrateAction = {
|
||||||
|
type: actions.MIGRATE_QUERY_EDITOR,
|
||||||
|
oldQueryEditor: qe,
|
||||||
|
newQueryEditor: { ...qe!, tabViewId, inLocalStorage: false },
|
||||||
|
};
|
||||||
|
newState = sqlLabReducer(newState, migrateAction as SqlLabAction);
|
||||||
|
|
||||||
|
// Restore SQL using tabViewId (as restoreSql in QueryTable does)
|
||||||
|
const sql = 'SELECT restored_query FROM history';
|
||||||
|
const restoreAction = {
|
||||||
|
type: actions.QUERY_EDITOR_SET_SQL,
|
||||||
|
queryEditor: { id: tabViewId },
|
||||||
|
sql,
|
||||||
|
};
|
||||||
|
newState = sqlLabReducer(newState, restoreAction);
|
||||||
|
expect(newState.unsavedQueryEditor.sql).toBe(sql);
|
||||||
|
expect(newState.unsavedQueryEditor.id).toBe(qe!.id);
|
||||||
|
});
|
||||||
test('should not fail while setting queryLimit', () => {
|
test('should not fail while setting queryLimit', () => {
|
||||||
const queryLimit = 101;
|
const queryLimit = 101;
|
||||||
const action = {
|
const action = {
|
||||||
|
|||||||
@@ -604,8 +604,20 @@ export default function sqlLabReducer(
|
|||||||
},
|
},
|
||||||
[actions.QUERY_EDITOR_SET_SQL]() {
|
[actions.QUERY_EDITOR_SET_SQL]() {
|
||||||
const { unsavedQueryEditor } = state;
|
const { unsavedQueryEditor } = state;
|
||||||
|
const actionId = action.queryEditor!.id!;
|
||||||
|
// Skip the O(n) tabViewId scan on the common path (keystroke: actionId already
|
||||||
|
// matches the active editor's client-side id). Only scan when ids differ, which
|
||||||
|
// happens when restoring from history with a backend-assigned tabViewId.
|
||||||
|
const normalizedId =
|
||||||
|
unsavedQueryEditor?.id === actionId
|
||||||
|
? actionId
|
||||||
|
: ((
|
||||||
|
getFromArr(state.queryEditors, actionId, 'tabViewId') as
|
||||||
|
| QueryEditor
|
||||||
|
| undefined
|
||||||
|
)?.id ?? actionId);
|
||||||
if (
|
if (
|
||||||
unsavedQueryEditor?.id === action.queryEditor!.id &&
|
unsavedQueryEditor?.id === normalizedId &&
|
||||||
unsavedQueryEditor.sql === action.sql
|
unsavedQueryEditor.sql === action.sql
|
||||||
) {
|
) {
|
||||||
return state;
|
return state;
|
||||||
@@ -618,7 +630,7 @@ export default function sqlLabReducer(
|
|||||||
sql: action.sql ?? undefined,
|
sql: action.sql ?? undefined,
|
||||||
...(action.queryId && { latestQueryId: action.queryId }),
|
...(action.queryId && { latestQueryId: action.queryId }),
|
||||||
},
|
},
|
||||||
action.queryEditor!.id!,
|
normalizedId,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
UPDATE_COMPONENTS,
|
UPDATE_COMPONENTS,
|
||||||
} from 'src/dashboard/actions/dashboardLayout';
|
} from 'src/dashboard/actions/dashboardLayout';
|
||||||
import { DASHBOARD_HEADER_ID } from 'src/dashboard/util/constants';
|
import { DASHBOARD_HEADER_ID } from 'src/dashboard/util/constants';
|
||||||
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
import DashboardPage from './DashboardPage';
|
import DashboardPage from './DashboardPage';
|
||||||
|
|
||||||
const mockTheme = {
|
const mockTheme = {
|
||||||
@@ -133,9 +134,11 @@ jest.mock('src/dashboard/util/activeDashboardFilters', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('src/utils/urlUtils', () => ({
|
jest.mock('src/utils/urlUtils', () => ({
|
||||||
getUrlParam: () => null,
|
getUrlParam: jest.fn().mockReturnValue(null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mockGetUrlParam = getUrlParam as jest.Mock;
|
||||||
|
|
||||||
jest.mock('src/dashboard/components/nativeFilters/FilterBar/keyValue', () => ({
|
jest.mock('src/dashboard/components/nativeFilters/FilterBar/keyValue', () => ({
|
||||||
getFilterValue: jest.fn(),
|
getFilterValue: jest.fn(),
|
||||||
getPermalinkValue: jest.fn(),
|
getPermalinkValue: jest.fn(),
|
||||||
@@ -161,6 +164,10 @@ beforeEach(() => {
|
|||||||
// Tests assert against the global document.title and the unmount restore
|
// Tests assert against the global document.title and the unmount restore
|
||||||
// effect can carry title state across tests, so reset it for isolation.
|
// effect can carry title state across tests, so reset it for isolation.
|
||||||
document.title = '';
|
document.title = '';
|
||||||
|
// clearAllMocks does not reset mockImplementation — reset explicitly so
|
||||||
|
// per-test overrides don't leak into subsequent tests.
|
||||||
|
mockGetUrlParam.mockReset();
|
||||||
|
mockGetUrlParam.mockReturnValue(null);
|
||||||
mockUseDashboard.mockReturnValue({
|
mockUseDashboard.mockReturnValue({
|
||||||
result: mockDashboard,
|
result: mockDashboard,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -443,6 +450,115 @@ test('passes null theme when Redux dashboardInfo.theme is explicitly null (theme
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('copies currentState to filterState for legacy native_filters URL params', async () => {
|
||||||
|
// Pre-2021 URLs encode filter selections under `currentState`. The dataMask
|
||||||
|
// reducer uses `filterState`, so without normalization the filter panel shows
|
||||||
|
// no active selections even though extraFormData still filters chart queries.
|
||||||
|
mockGetUrlParam.mockImplementation((param: { name: string }) => {
|
||||||
|
if (param.name === 'native_filters') {
|
||||||
|
return {
|
||||||
|
'NATIVE_FILTER-OvPTDNKc9': {
|
||||||
|
extraFormData: {
|
||||||
|
filters: [{ col: 'team_name', op: 'IN', val: ['MarginEdge'] }],
|
||||||
|
},
|
||||||
|
currentState: { value: ['MarginEdge'] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Suspense fallback="loading">
|
||||||
|
<DashboardPage idOrSlug="1" />
|
||||||
|
</Suspense>,
|
||||||
|
{
|
||||||
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
|
initialState: {
|
||||||
|
dashboardInfo: { id: 1, metadata: {} },
|
||||||
|
dashboardState: { sliceIds: [] },
|
||||||
|
nativeFilters: { filters: {} },
|
||||||
|
dataMask: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('loading')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hydrateDashboard).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
dataMask: expect.objectContaining({
|
||||||
|
'NATIVE_FILTER-OvPTDNKc9': expect.objectContaining({
|
||||||
|
filterState: { value: ['MarginEdge'] },
|
||||||
|
extraFormData: {
|
||||||
|
filters: [{ col: 'team_name', op: 'IN', val: ['MarginEdge'] }],
|
||||||
|
},
|
||||||
|
currentState: { value: ['MarginEdge'] },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not overwrite filterState when modern native_filters URL format is used', async () => {
|
||||||
|
// Modern URLs already carry `filterState`; the normalization must not clobber it.
|
||||||
|
mockGetUrlParam.mockImplementation((param: { name: string }) => {
|
||||||
|
if (param.name === 'native_filters') {
|
||||||
|
return {
|
||||||
|
'NATIVE_FILTER-OvPTDNKc9': {
|
||||||
|
extraFormData: {
|
||||||
|
filters: [{ col: 'team_name', op: 'IN', val: ['MarginEdge'] }],
|
||||||
|
},
|
||||||
|
filterState: { value: ['MarginEdge'] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Suspense fallback="loading">
|
||||||
|
<DashboardPage idOrSlug="1" />
|
||||||
|
</Suspense>,
|
||||||
|
{
|
||||||
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
|
initialState: {
|
||||||
|
dashboardInfo: { id: 1, metadata: {} },
|
||||||
|
dashboardState: { sliceIds: [] },
|
||||||
|
nativeFilters: { filters: {} },
|
||||||
|
dataMask: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('loading')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hydrateDashboard).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
dataMask: expect.objectContaining({
|
||||||
|
'NATIVE_FILTER-OvPTDNKc9': expect.objectContaining({
|
||||||
|
filterState: { value: ['MarginEdge'] },
|
||||||
|
extraFormData: {
|
||||||
|
filters: [{ col: 'team_name', op: 'IN', val: ['MarginEdge'] }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// currentState must not have been injected
|
||||||
|
const callArg = (hydrateDashboard as jest.Mock).mock.calls[0][0];
|
||||||
|
expect(
|
||||||
|
callArg.dataMask['NATIVE_FILTER-OvPTDNKc9'].currentState,
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
test('clears undo history after hydrating the dashboard', async () => {
|
test('clears undo history after hydrating the dashboard', async () => {
|
||||||
render(
|
render(
|
||||||
<Suspense fallback="loading">
|
<Suspense fallback="loading">
|
||||||
|
|||||||
@@ -223,7 +223,22 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
|||||||
dataMask = await getFilterValue(id, nativeFilterKeyValue);
|
dataMask = await getFilterValue(id, nativeFilterKeyValue);
|
||||||
}
|
}
|
||||||
if (isOldRison) {
|
if (isOldRison) {
|
||||||
dataMask = isOldRison;
|
// Normalize legacy `currentState` → `filterState`. Pre-2021 URLs stored
|
||||||
|
// per-filter selections under `currentState`; modern dataMask uses
|
||||||
|
// `filterState`. Without this copy the filter panel shows no active
|
||||||
|
// selections even though extraFormData still applies the query filter.
|
||||||
|
if (typeof isOldRison === 'object' && isOldRison !== null) {
|
||||||
|
dataMask = Object.fromEntries(
|
||||||
|
Object.entries(
|
||||||
|
isOldRison as Record<string, Record<string, unknown>>,
|
||||||
|
).map(([filterId, entry]) => [
|
||||||
|
filterId,
|
||||||
|
entry?.currentState && !entry?.filterState
|
||||||
|
? { ...entry, filterState: entry.currentState }
|
||||||
|
: entry,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Rison URL filters with intelligent native filter injection
|
// Parse Rison URL filters with intelligent native filter injection
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import type { AnyAction } from 'redux';
|
||||||
|
// eslint-disable-next-line import/named
|
||||||
|
import {
|
||||||
|
ActionCreators as UndoActionCreators,
|
||||||
|
StateWithHistory,
|
||||||
|
} from 'redux-undo';
|
||||||
|
|
||||||
|
import undoableLayoutReducer from 'src/dashboard/reducers/undoableDashboardLayout';
|
||||||
|
import { UPDATE_COMPONENTS } from 'src/dashboard/actions/dashboardLayout';
|
||||||
|
import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
|
||||||
|
import type { DashboardLayout } from 'src/dashboard/types';
|
||||||
|
import {
|
||||||
|
DASHBOARD_ROOT_ID,
|
||||||
|
DASHBOARD_GRID_ID,
|
||||||
|
DASHBOARD_HEADER_ID,
|
||||||
|
} from 'src/dashboard/util/constants';
|
||||||
|
import {
|
||||||
|
DASHBOARD_ROOT_TYPE,
|
||||||
|
DASHBOARD_GRID_TYPE,
|
||||||
|
DASHBOARD_HEADER_TYPE,
|
||||||
|
CHART_TYPE,
|
||||||
|
} from 'src/dashboard/util/componentTypes';
|
||||||
|
|
||||||
|
const reducer = undoableLayoutReducer;
|
||||||
|
|
||||||
|
// A minimal but valid dashboard layout always contains the root component.
|
||||||
|
const makeValidLayout = (
|
||||||
|
title = '[ untitled dashboard ]',
|
||||||
|
): DashboardLayout => ({
|
||||||
|
[DASHBOARD_ROOT_ID]: {
|
||||||
|
id: DASHBOARD_ROOT_ID,
|
||||||
|
type: DASHBOARD_ROOT_TYPE,
|
||||||
|
children: [DASHBOARD_GRID_ID],
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
[DASHBOARD_GRID_ID]: {
|
||||||
|
id: DASHBOARD_GRID_ID,
|
||||||
|
type: DASHBOARD_GRID_TYPE,
|
||||||
|
parents: [DASHBOARD_ROOT_ID],
|
||||||
|
children: [],
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
[DASHBOARD_HEADER_ID]: {
|
||||||
|
id: DASHBOARD_HEADER_ID,
|
||||||
|
type: DASHBOARD_HEADER_TYPE,
|
||||||
|
children: [],
|
||||||
|
meta: { text: title },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// The frontend locks redux-undo to 1.1.0, whose `clearHistory()` under
|
||||||
|
// `ignoreInitialState` resets `_latestUnfiltered` to null. That makes a rootless
|
||||||
|
// layout impossible to push onto `past` through normal layout actions, so the
|
||||||
|
// guard's corrupt-history precondition is seeded directly. `makeHistory` mirrors
|
||||||
|
// redux-undo's `StateWithHistory` shape — `past`/`present`/`future` is all that
|
||||||
|
// `undo()` needs to compute the previous state.
|
||||||
|
const makeHistory = (
|
||||||
|
past: DashboardLayout[],
|
||||||
|
present: DashboardLayout,
|
||||||
|
future: DashboardLayout[] = [],
|
||||||
|
): StateWithHistory<DashboardLayout> => ({ past, present, future });
|
||||||
|
|
||||||
|
const hydrate = (present: DashboardLayout): AnyAction => ({
|
||||||
|
type: HYDRATE_DASHBOARD,
|
||||||
|
data: { dashboardLayout: { present } },
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hydrating a dashboard leaves an empty, disabled undo history', () => {
|
||||||
|
const initial = reducer(undefined, { type: '@@INIT' });
|
||||||
|
const state = reducer(initial, hydrate(makeValidLayout()));
|
||||||
|
|
||||||
|
expect(state.present[DASHBOARD_ROOT_ID]).toBeDefined();
|
||||||
|
// Hydration is not a user edit, so Undo (past) and Redo (future) start empty.
|
||||||
|
expect(state.past).toHaveLength(0);
|
||||||
|
expect(state.future).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a layout edit is applied through the wrapped reducer', () => {
|
||||||
|
const hydrated = reducer(
|
||||||
|
reducer(undefined, { type: '@@INIT' }),
|
||||||
|
hydrate(makeValidLayout()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const update: AnyAction = {
|
||||||
|
type: UPDATE_COMPONENTS,
|
||||||
|
payload: {
|
||||||
|
nextComponents: {
|
||||||
|
'CHART-1': { id: 'CHART-1', type: CHART_TYPE, children: [], meta: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const state = reducer(hydrated, update);
|
||||||
|
|
||||||
|
expect(state.present['CHART-1']).toBeDefined();
|
||||||
|
expect(state.present[DASHBOARD_ROOT_ID]).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('re-hydrating a different dashboard clears the previous dashboard from the undo stack', () => {
|
||||||
|
// Simulates SPA navigation: dashboard A already has undo history when B opens.
|
||||||
|
const dashboardA = makeHistory(
|
||||||
|
[makeValidLayout('A v1')],
|
||||||
|
makeValidLayout('A v2'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const state = reducer(dashboardA, hydrate(makeValidLayout('B')));
|
||||||
|
|
||||||
|
expect(state.present[DASHBOARD_ROOT_ID]).toBeDefined();
|
||||||
|
expect(state.past).toHaveLength(0);
|
||||||
|
expect(state.future).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('undo never reverts the layout to an invalid (rootless) state', () => {
|
||||||
|
// A rootless `{}` baseline sits at the head of `past`; a plain redux-undo
|
||||||
|
// undo() here would move it into `present` and crash rendering with
|
||||||
|
// `Cannot read properties of undefined (reading 'type')`.
|
||||||
|
const corrupt = makeHistory([{}], makeValidLayout());
|
||||||
|
const before = corrupt.present;
|
||||||
|
|
||||||
|
const state = reducer(corrupt, UndoActionCreators.undo());
|
||||||
|
|
||||||
|
// The guard rejects the transition: the valid layout is kept unchanged...
|
||||||
|
expect(state.present[DASHBOARD_ROOT_ID]).toBeDefined();
|
||||||
|
expect(state.present).toBe(before);
|
||||||
|
// ...and history is left intact, so undoLayoutAction() won't misread an
|
||||||
|
// emptied stack as a fully-reverted, clean dashboard.
|
||||||
|
expect(state.past).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the guard does not interfere with a normal undo between valid layouts', () => {
|
||||||
|
const previous = makeValidLayout('previous');
|
||||||
|
const current = makeValidLayout('current');
|
||||||
|
|
||||||
|
const state = reducer(
|
||||||
|
makeHistory([previous], current),
|
||||||
|
UndoActionCreators.undo(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// A valid -> valid undo proceeds normally.
|
||||||
|
expect(state.present).toBe(previous);
|
||||||
|
expect(state.past).toHaveLength(0);
|
||||||
|
expect(state.future).toHaveLength(1);
|
||||||
|
});
|
||||||
@@ -18,8 +18,11 @@
|
|||||||
*/
|
*/
|
||||||
import { AnyAction, Reducer } from 'redux';
|
import { AnyAction, Reducer } from 'redux';
|
||||||
// eslint-disable-next-line import/named
|
// eslint-disable-next-line import/named
|
||||||
import undoable, { StateWithHistory } from 'redux-undo';
|
import undoable, {
|
||||||
import { UNDO_LIMIT } from '../util/constants';
|
ActionCreators as UndoActionCreators,
|
||||||
|
StateWithHistory,
|
||||||
|
} from 'redux-undo';
|
||||||
|
import { DASHBOARD_ROOT_ID, UNDO_LIMIT } from '../util/constants';
|
||||||
import {
|
import {
|
||||||
UPDATE_COMPONENTS,
|
UPDATE_COMPONENTS,
|
||||||
DELETE_COMPONENT,
|
DELETE_COMPONENT,
|
||||||
@@ -97,7 +100,7 @@ const layoutOnlyReducer: Reducer<DashboardLayout, AnyAction> = (
|
|||||||
return dashboardLayout(state || {}, action);
|
return dashboardLayout(state || {}, action);
|
||||||
};
|
};
|
||||||
|
|
||||||
const undoableReducer: Reducer<
|
const baseUndoableReducer: Reducer<
|
||||||
StateWithHistory<DashboardLayout>,
|
StateWithHistory<DashboardLayout>,
|
||||||
AnyAction
|
AnyAction
|
||||||
> = undoable(layoutOnlyReducer, {
|
> = undoable(layoutOnlyReducer, {
|
||||||
@@ -107,4 +110,53 @@ const undoableReducer: Reducer<
|
|||||||
ignoreInitialState: true,
|
ignoreInitialState: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A valid dashboard layout always contains the root component. Undo/redo must
|
||||||
|
* never leave `present` without it: a rootless layout renders the dashboard
|
||||||
|
* with no components and throws
|
||||||
|
* `TypeError: Cannot read properties of undefined (reading 'type')`. Such a
|
||||||
|
* state can arise whenever a rootless or empty layout reaches the undo history —
|
||||||
|
* e.g. an empty or partial hydration, or a tracked layout action dispatched
|
||||||
|
* before the dashboard has hydrated.
|
||||||
|
*/
|
||||||
|
const isValidLayout = (layout?: DashboardLayout): boolean =>
|
||||||
|
Boolean(layout && layout[DASHBOARD_ROOT_ID]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Wraps the redux-undo reducer to keep the dashboard layout undo history sound:
|
||||||
|
*
|
||||||
|
* 1. Hydration establishes the baseline for the dashboard being opened. It is
|
||||||
|
* not a user edit and must never be undoable, so the history is reset on
|
||||||
|
* every HYDRATE_DASHBOARD. Doing this in the reducer — rather than relying
|
||||||
|
* solely on a follow-up clearDashboardHistory() dispatch from the page
|
||||||
|
* component — guarantees the Undo control starts disabled and that no layout
|
||||||
|
* from a previously edited dashboard lingers in the stack after navigation.
|
||||||
|
* 2. As defense in depth, undo/redo is never allowed to replace a valid layout
|
||||||
|
* with an invalid (rootless) one. Such a transition is rejected and the
|
||||||
|
* current valid layout is kept, so clicking Undo can never crash the
|
||||||
|
* dashboard. History is left untouched on rejection so callers that inspect
|
||||||
|
* it (e.g. undoLayoutAction) don't misread an emptied stack as a clean,
|
||||||
|
* fully-reverted dashboard and silently drop the unsaved-changes guard.
|
||||||
|
*/
|
||||||
|
const undoableReducer: Reducer<StateWithHistory<DashboardLayout>, AnyAction> = (
|
||||||
|
state,
|
||||||
|
action,
|
||||||
|
) => {
|
||||||
|
const nextState = baseUndoableReducer(state, action);
|
||||||
|
|
||||||
|
if (action.type === HYDRATE_DASHBOARD) {
|
||||||
|
return baseUndoableReducer(nextState, UndoActionCreators.clearHistory());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
state &&
|
||||||
|
isValidLayout(state.present) &&
|
||||||
|
!isValidLayout(nextState.present)
|
||||||
|
) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextState;
|
||||||
|
};
|
||||||
|
|
||||||
export default undoableReducer;
|
export default undoableReducer;
|
||||||
|
|||||||
@@ -16,27 +16,17 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useMemo, useState, useEffect, useRef, RefObject } from 'react';
|
import { useMemo, useEffect, useRef, RefObject } from 'react';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { getTimeFormatter, safeHtmlSpan, TimeFormats } from '@superset-ui/core';
|
|
||||||
import { css, styled, useTheme } from '@apache-superset/core/theme';
|
import { css, styled, useTheme } from '@apache-superset/core/theme';
|
||||||
import { GenericDataType } from '@apache-superset/core/common';
|
|
||||||
import { Column } from 'react-table';
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import {
|
import { Constants, Button, Icons, Input } from '@superset-ui/core/components';
|
||||||
Constants,
|
|
||||||
Button,
|
|
||||||
Icons,
|
|
||||||
Input,
|
|
||||||
Popover,
|
|
||||||
Radio,
|
|
||||||
} from '@superset-ui/core/components';
|
|
||||||
import { CopyToClipboard } from 'src/components';
|
import { CopyToClipboard } from 'src/components';
|
||||||
import {
|
import {
|
||||||
prepareCopyToClipboardTabularData,
|
prepareCopyToClipboardTabularData,
|
||||||
TabularDataRow,
|
TabularDataRow,
|
||||||
} from 'src/utils/common';
|
} from 'src/utils/common';
|
||||||
import { getTimeColumns, setTimeColumns } from './utils';
|
|
||||||
|
|
||||||
export const CellNull = styled('span')`
|
export const CellNull = styled('span')`
|
||||||
color: ${({ theme }) => theme.colorTextTertiary};
|
color: ${({ theme }) => theme.colorTextTertiary};
|
||||||
@@ -136,113 +126,6 @@ export const FilterInput = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
enum FormatPickerValue {
|
|
||||||
Formatted = 'formatted',
|
|
||||||
Original = 'original',
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormatPicker = ({
|
|
||||||
onChange,
|
|
||||||
value,
|
|
||||||
}: {
|
|
||||||
onChange: any;
|
|
||||||
value: FormatPickerValue;
|
|
||||||
}) => (
|
|
||||||
<Radio.GroupWrapper
|
|
||||||
spaceConfig={{
|
|
||||||
direction: 'vertical',
|
|
||||||
align: 'start',
|
|
||||||
size: 15,
|
|
||||||
wrap: false,
|
|
||||||
}}
|
|
||||||
size="large"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
options={[
|
|
||||||
{ label: t('Formatted date'), value: FormatPickerValue.Formatted },
|
|
||||||
{ label: t('Original value'), value: FormatPickerValue.Original },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const FormatPickerContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
padding: ${({ theme }) => `${theme.sizeUnit * 4}px`};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FormatPickerLabel = styled.span`
|
|
||||||
font-size: ${({ theme }) => theme.fontSizeSM}px;
|
|
||||||
color: ${({ theme }) => theme.colorText};
|
|
||||||
margin-bottom: ${({ theme }) => theme.sizeUnit * 2}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DataTableTemporalHeaderCell = ({
|
|
||||||
columnName,
|
|
||||||
onTimeColumnChange,
|
|
||||||
datasourceId,
|
|
||||||
isOriginalTimeColumn,
|
|
||||||
displayLabel,
|
|
||||||
}: {
|
|
||||||
columnName: string;
|
|
||||||
onTimeColumnChange: (
|
|
||||||
columnName: string,
|
|
||||||
columnType: FormatPickerValue,
|
|
||||||
) => void;
|
|
||||||
datasourceId?: string;
|
|
||||||
isOriginalTimeColumn: boolean;
|
|
||||||
displayLabel?: string;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const onChange = (e: any) => {
|
|
||||||
onTimeColumnChange(columnName, e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const overlayContent = useMemo(
|
|
||||||
() =>
|
|
||||||
datasourceId ? ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
||||||
<FormatPickerContainer
|
|
||||||
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{/* hack to disable click propagation from popover content to table header, which triggers sorting column */}
|
|
||||||
<FormatPickerLabel>{t('Column Formatting')}</FormatPickerLabel>
|
|
||||||
<FormatPicker
|
|
||||||
onChange={onChange}
|
|
||||||
value={
|
|
||||||
isOriginalTimeColumn
|
|
||||||
? FormatPickerValue.Original
|
|
||||||
: FormatPickerValue.Formatted
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormatPickerContainer>
|
|
||||||
) : null,
|
|
||||||
[datasourceId, isOriginalTimeColumn],
|
|
||||||
);
|
|
||||||
|
|
||||||
return datasourceId ? (
|
|
||||||
<span>
|
|
||||||
<Popover
|
|
||||||
trigger="click"
|
|
||||||
content={overlayContent}
|
|
||||||
placement="bottomLeft"
|
|
||||||
arrow={{ pointAtCenter: true }}
|
|
||||||
>
|
|
||||||
<Icons.SettingOutlined
|
|
||||||
iconSize="m"
|
|
||||||
iconColor={theme.colorIcon}
|
|
||||||
css={{ marginRight: `${theme.sizeUnit}px` }}
|
|
||||||
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
{displayLabel ?? columnName}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>{displayLabel ?? columnName}</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useFilteredTableData = (
|
export const useFilteredTableData = (
|
||||||
filterText: string,
|
filterText: string,
|
||||||
data?: Record<string, any>[],
|
data?: Record<string, any>[],
|
||||||
@@ -268,120 +151,3 @@ export const useFilteredTableData = (
|
|||||||
);
|
);
|
||||||
}, [data, filterText, rowsAsStrings]);
|
}, [data, filterText, rowsAsStrings]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeFormatter = getTimeFormatter(TimeFormats.DATABASE_DATETIME);
|
|
||||||
|
|
||||||
export const useTableColumns = (
|
|
||||||
colnames?: string[],
|
|
||||||
coltypes?: GenericDataType[],
|
|
||||||
data?: Record<string, any>[],
|
|
||||||
datasourceId?: string,
|
|
||||||
isVisible?: boolean,
|
|
||||||
moreConfigs?: { [key: string]: Partial<Column> },
|
|
||||||
allowHTML?: boolean,
|
|
||||||
columnDisplayNames?: Record<string, string>,
|
|
||||||
) => {
|
|
||||||
const [originalFormattedTimeColumns, setOriginalFormattedTimeColumns] =
|
|
||||||
useState<string[]>(getTimeColumns(datasourceId));
|
|
||||||
|
|
||||||
const onTimeColumnChange = (
|
|
||||||
columnName: string,
|
|
||||||
columnType: FormatPickerValue,
|
|
||||||
) => {
|
|
||||||
if (!datasourceId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
columnType === FormatPickerValue.Original &&
|
|
||||||
!originalFormattedTimeColumns.includes(columnName)
|
|
||||||
) {
|
|
||||||
const cols = getTimeColumns(datasourceId);
|
|
||||||
cols.push(columnName);
|
|
||||||
setTimeColumns(datasourceId, cols);
|
|
||||||
setOriginalFormattedTimeColumns(cols);
|
|
||||||
} else if (
|
|
||||||
columnType === FormatPickerValue.Formatted &&
|
|
||||||
originalFormattedTimeColumns.includes(columnName)
|
|
||||||
) {
|
|
||||||
const cols = getTimeColumns(datasourceId);
|
|
||||||
cols.splice(cols.indexOf(columnName), 1);
|
|
||||||
setTimeColumns(datasourceId, cols);
|
|
||||||
setOriginalFormattedTimeColumns(cols);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isVisible) {
|
|
||||||
setOriginalFormattedTimeColumns(getTimeColumns(datasourceId));
|
|
||||||
}
|
|
||||||
}, [datasourceId, isVisible]);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
colnames && data?.length
|
|
||||||
? colnames
|
|
||||||
.filter((column: string) => Object.keys(data[0]).includes(column))
|
|
||||||
.map((key, index) => {
|
|
||||||
const colType = coltypes?.[index];
|
|
||||||
const firstValue = data[0][key];
|
|
||||||
const headerLabel = columnDisplayNames?.[key] ?? key;
|
|
||||||
const originalFormattedTimeColumnIndex =
|
|
||||||
colType === GenericDataType.Temporal
|
|
||||||
? originalFormattedTimeColumns.indexOf(key)
|
|
||||||
: -1;
|
|
||||||
const isOriginalTimeColumn =
|
|
||||||
originalFormattedTimeColumns.includes(key);
|
|
||||||
return {
|
|
||||||
// react-table requires a non-empty id, therefore we introduce a fallback value in case the key is empty
|
|
||||||
id: key || String(index),
|
|
||||||
accessor: (row: Record<string, any>) => row[key],
|
|
||||||
Header:
|
|
||||||
colType === GenericDataType.Temporal &&
|
|
||||||
typeof firstValue !== 'string' ? (
|
|
||||||
<DataTableTemporalHeaderCell
|
|
||||||
columnName={key}
|
|
||||||
datasourceId={datasourceId}
|
|
||||||
onTimeColumnChange={onTimeColumnChange}
|
|
||||||
isOriginalTimeColumn={isOriginalTimeColumn}
|
|
||||||
displayLabel={headerLabel}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
headerLabel
|
|
||||||
),
|
|
||||||
Cell: ({ value }) => {
|
|
||||||
if (value === true) {
|
|
||||||
return Constants.BOOL_TRUE_DISPLAY;
|
|
||||||
}
|
|
||||||
if (value === false) {
|
|
||||||
return Constants.BOOL_FALSE_DISPLAY;
|
|
||||||
}
|
|
||||||
if (value === null) {
|
|
||||||
return <CellNull>{Constants.NULL_DISPLAY}</CellNull>;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
colType === GenericDataType.Temporal &&
|
|
||||||
originalFormattedTimeColumnIndex === -1 &&
|
|
||||||
typeof value === 'number'
|
|
||||||
) {
|
|
||||||
return timeFormatter(value);
|
|
||||||
}
|
|
||||||
if (typeof value === 'string' && allowHTML) {
|
|
||||||
return safeHtmlSpan(value);
|
|
||||||
}
|
|
||||||
return String(value);
|
|
||||||
},
|
|
||||||
...moreConfigs?.[key],
|
|
||||||
} as Column;
|
|
||||||
})
|
|
||||||
: [],
|
|
||||||
[
|
|
||||||
colnames,
|
|
||||||
data,
|
|
||||||
coltypes,
|
|
||||||
datasourceId,
|
|
||||||
moreConfigs,
|
|
||||||
originalFormattedTimeColumns,
|
|
||||||
columnDisplayNames,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import { GenericDataType } from '@apache-superset/core/common';
|
|
||||||
import { renderHook } from '@testing-library/react';
|
|
||||||
import { Constants } from '@superset-ui/core/components';
|
|
||||||
import { useTableColumns } from '.';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
type JsonObject = { [member: string]: any };
|
|
||||||
|
|
||||||
const asciiChars = [];
|
|
||||||
for (let i = 32; i < 127; i += 1) {
|
|
||||||
asciiChars.push(String.fromCharCode(i));
|
|
||||||
}
|
|
||||||
const ASCII_KEY = asciiChars.join('');
|
|
||||||
const UNICODE_KEY = '你好. 吃了吗?';
|
|
||||||
const NUMTIME_KEY = 'numtime';
|
|
||||||
const STRTIME_KEY = 'strtime';
|
|
||||||
const NUMTIME_VALUE = 1640995200000;
|
|
||||||
const NUMTIME_FORMATTED_VALUE = '2022-01-01 00:00:00';
|
|
||||||
const STRTIME_VALUE = '2022-01-01';
|
|
||||||
|
|
||||||
const colnames = [
|
|
||||||
'col01',
|
|
||||||
'col02',
|
|
||||||
ASCII_KEY,
|
|
||||||
UNICODE_KEY,
|
|
||||||
NUMTIME_KEY,
|
|
||||||
STRTIME_KEY,
|
|
||||||
];
|
|
||||||
const coltypes = [
|
|
||||||
GenericDataType.Boolean,
|
|
||||||
GenericDataType.Boolean,
|
|
||||||
GenericDataType.String,
|
|
||||||
GenericDataType.String,
|
|
||||||
GenericDataType.Temporal,
|
|
||||||
GenericDataType.Temporal,
|
|
||||||
];
|
|
||||||
|
|
||||||
const cellValues = {
|
|
||||||
col01: true,
|
|
||||||
col02: false,
|
|
||||||
[ASCII_KEY]: ASCII_KEY,
|
|
||||||
[UNICODE_KEY]: UNICODE_KEY,
|
|
||||||
[NUMTIME_KEY]: NUMTIME_VALUE,
|
|
||||||
[STRTIME_KEY]: STRTIME_VALUE,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = [cellValues, cellValues, cellValues, cellValues];
|
|
||||||
|
|
||||||
const expectedDisplayValues = {
|
|
||||||
col01: Constants.BOOL_TRUE_DISPLAY,
|
|
||||||
col02: Constants.BOOL_FALSE_DISPLAY,
|
|
||||||
[ASCII_KEY]: ASCII_KEY,
|
|
||||||
[UNICODE_KEY]: UNICODE_KEY,
|
|
||||||
[NUMTIME_KEY]: NUMTIME_FORMATTED_VALUE,
|
|
||||||
[STRTIME_KEY]: STRTIME_VALUE,
|
|
||||||
};
|
|
||||||
|
|
||||||
test('useTableColumns with no options', () => {
|
|
||||||
const hook = renderHook(() => useTableColumns(colnames, coltypes, data));
|
|
||||||
expect(hook.result.current).toMatchInlineSnapshot(`
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": "col01",
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": "col01",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": "col02",
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": "col02",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": " !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~",
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": " !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": "你好. 吃了吗?",
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": "你好. 吃了吗?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": <DataTableTemporalHeaderCell
|
|
||||||
columnName="numtime"
|
|
||||||
displayLabel="numtime"
|
|
||||||
isOriginalTimeColumn={false}
|
|
||||||
onTimeColumnChange={[Function]}
|
|
||||||
/>,
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": "numtime",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": "strtime",
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": "strtime",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`);
|
|
||||||
hook.result.current.forEach((col: JsonObject) => {
|
|
||||||
expect(col.accessor(data[0])).toBe(data[0][col.id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
hook.result.current.forEach((col: JsonObject) => {
|
|
||||||
data.forEach(row => {
|
|
||||||
expect(col.Cell({ value: row[col.id] })).toBe(
|
|
||||||
expectedDisplayValues[col.id],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('useTableColumns with options', () => {
|
|
||||||
const hook = renderHook(() =>
|
|
||||||
useTableColumns(colnames, coltypes, data, undefined, true, {
|
|
||||||
col01: { Header: 'Header' },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(hook.result.current).toMatchInlineSnapshot(`
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": "Header",
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": "col01",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": "col02",
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": "col02",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": " !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~",
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": " !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": "你好. 吃了吗?",
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": "你好. 吃了吗?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": <DataTableTemporalHeaderCell
|
|
||||||
columnName="numtime"
|
|
||||||
displayLabel="numtime"
|
|
||||||
isOriginalTimeColumn={false}
|
|
||||||
onTimeColumnChange={[Function]}
|
|
||||||
/>,
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": "numtime",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Cell": [Function],
|
|
||||||
"Header": "strtime",
|
|
||||||
"accessor": [Function],
|
|
||||||
"id": "strtime",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`);
|
|
||||||
hook.result.current.forEach((col: JsonObject) => {
|
|
||||||
expect(col.accessor(data[0])).toBe(data[0][col.id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
hook.result.current.forEach((col: JsonObject) => {
|
|
||||||
data.forEach(row => {
|
|
||||||
expect(col.Cell({ value: row[col.id] })).toBe(
|
|
||||||
expectedDisplayValues[col.id],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('useTableColumns applies columnDisplayNames to headers', () => {
|
|
||||||
const columnDisplayNames = {
|
|
||||||
col01: 'Column One',
|
|
||||||
[NUMTIME_KEY]: 'Verbose Numtime',
|
|
||||||
} as Record<string, string>;
|
|
||||||
const hook = renderHook(() =>
|
|
||||||
useTableColumns(
|
|
||||||
colnames,
|
|
||||||
coltypes,
|
|
||||||
data,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
columnDisplayNames,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const cols = hook.result.current as JsonObject[];
|
|
||||||
const col01 = cols.find(c => c.id === 'col01');
|
|
||||||
const numtime = cols.find(c => c.id === NUMTIME_KEY);
|
|
||||||
expect(col01?.Header).toBe('Column One');
|
|
||||||
// Temporal header is a component; ensure it received the displayLabel prop
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
expect(numtime?.Header.props.displayLabel).toBe('Verbose Numtime');
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user