mirror of
https://github.com/apache/superset.git
synced 2026-06-26 18:09:21 +00:00
Compare commits
3 Commits
chore/ci/s
...
fix-clickh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05182f95a0 | ||
|
|
ace7a65716 | ||
|
|
0a40a94ab2 |
4
.github/workflows/bump-python-package.yml
vendored
4
.github/workflows/bump-python-package.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
checks: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: master
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-supersetbot/
|
||||
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
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
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- 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 }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
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
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Dependency Review"
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
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 }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- 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:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
# 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:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
|
||||
2
.github/workflows/generate-FOSSA-report.yml
vendored
2
.github/workflows/generate-FOSSA-report.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/issue_creation.yml
vendored
2
.github/workflows/issue_creation.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
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:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/license-check.yml
vendored
2
.github/workflows/license-check.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/pr-lint.yml
vendored
2
.github/workflows/pr-lint.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
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"]') }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
# 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)
|
||||
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ steps.check.outputs.target_sha }}
|
||||
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
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/superset-docs-deploy.yml
vendored
2
.github/workflows/superset-docs-deploy.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
6
.github/workflows/superset-docs-verify.yml
vendored
6
.github/workflows/superset-docs-verify.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
name: Link Checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Do not bump this linkinator-action version without opening
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
working-directory: docs
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
working-directory: docs
|
||||
steps:
|
||||
- name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
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 }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -97,21 +97,21 @@ jobs:
|
||||
# Conditional checkout based on context
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
@@ -207,21 +207,21 @@ jobs:
|
||||
# Conditional checkout based on context (same as Cypress workflow)
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
working-directory: superset-extensions-cli
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
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 }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
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
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/superset-helm-release.yml
vendored
2
.github/workflows/superset-helm-release.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref_name }}
|
||||
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 }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -83,21 +83,21 @@ jobs:
|
||||
# Conditional checkout based on context (same as Cypress workflow)
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
4
.github/workflows/superset-translations.yml
vendored
4
.github/workflows/superset-translations.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/superset-websocket.yml
vendored
2
.github/workflows/superset-websocket.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install dependencies
|
||||
|
||||
2
.github/workflows/supersetbot.yml
vendored
2
.github/workflows/supersetbot.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
});
|
||||
|
||||
- name: "Checkout ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
# zizmor: ignore[artipacked] - required persisted credentials to push synced requirement changes back to remote
|
||||
- name: Checkout source code
|
||||
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
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
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
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
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#
|
||||
HYPHEN_SYMBOL='-'
|
||||
|
||||
exec gunicorn \
|
||||
gunicorn \
|
||||
--bind "${SUPERSET_BIND_ADDRESS:-0.0.0.0}:${SUPERSET_PORT:-8088}" \
|
||||
--access-logfile "${ACCESS_LOG_FILE:-$HYPHEN_SYMBOL}" \
|
||||
--error-logfile "${ERROR_LOG_FILE:-$HYPHEN_SYMBOL}" \
|
||||
|
||||
@@ -1808,10 +1808,6 @@ 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.
|
||||
|
||||
:::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:
|
||||
|
||||
```json
|
||||
|
||||
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.17.2 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.17.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
@@ -216,7 +216,6 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
||||
| supersetNode.extraContainers | list | `[]` | Launch additional containers into supersetNode pod |
|
||||
| supersetNode.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
|
||||
| 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.httpGet.path | string | `"/health"` | |
|
||||
| supersetNode.livenessProbe.httpGet.port | string | `"http"` | |
|
||||
@@ -249,7 +248,6 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
||||
| supersetNode.startupProbe.successThreshold | int | `1` | |
|
||||
| supersetNode.startupProbe.timeoutSeconds | int | `1` | |
|
||||
| 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 |
|
||||
| supersetWebsockets.affinity | object | `{}` | Affinity to be added to supersetWebsockets deployment |
|
||||
| supersetWebsockets.command | list | `[]` | |
|
||||
@@ -313,7 +311,6 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
||||
| supersetWorker.extraContainers | list | `[]` | Launch additional containers into supersetWorker pod |
|
||||
| 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.lifecycle | object | `{}` | Container lifecycle hooks for the worker pod |
|
||||
| supersetWorker.livenessProbe.exec.command | list | a `celery inspect ping` command | Liveness probe command |
|
||||
| supersetWorker.livenessProbe.failureThreshold | int | `3` | |
|
||||
| supersetWorker.livenessProbe.initialDelaySeconds | int | `120` | |
|
||||
@@ -334,7 +331,6 @@ 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.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.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 |
|
||||
| tolerations | list | `[]` | |
|
||||
| topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to all deployments |
|
||||
|
||||
@@ -134,9 +134,6 @@ spec:
|
||||
{{- if .Values.supersetWorker.livenessProbe }}
|
||||
livenessProbe: {{- .Values.supersetWorker.livenessProbe | toYaml | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetWorker.lifecycle }}
|
||||
lifecycle: {{- .Values.supersetWorker.lifecycle | toYaml | nindent 12 }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- if .Values.supersetWorker.resources }}
|
||||
{{- toYaml .Values.supersetWorker.resources | nindent 12 }}
|
||||
@@ -173,9 +170,6 @@ spec:
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetWorker.terminationGracePeriodSeconds }}
|
||||
terminationGracePeriodSeconds: {{ .Values.supersetWorker.terminationGracePeriodSeconds }}
|
||||
{{- end }}
|
||||
{{- if .Values.imagePullSecrets }}
|
||||
imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -144,9 +144,6 @@ spec:
|
||||
{{- if .Values.supersetNode.livenessProbe }}
|
||||
livenessProbe: {{- .Values.supersetNode.livenessProbe | toYaml | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetNode.lifecycle }}
|
||||
lifecycle: {{- .Values.supersetNode.lifecycle | toYaml | nindent 12 }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- if .Values.supersetNode.resources }}
|
||||
{{- toYaml .Values.supersetNode.resources | nindent 12 }}
|
||||
@@ -183,9 +180,6 @@ spec:
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetNode.terminationGracePeriodSeconds }}
|
||||
terminationGracePeriodSeconds: {{ .Values.supersetNode.terminationGracePeriodSeconds }}
|
||||
{{- end }}
|
||||
{{- if .Values.imagePullSecrets }}
|
||||
imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -269,7 +269,7 @@ supersetNode:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec /usr/bin/run-server.sh"
|
||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; /usr/bin/run-server.sh"
|
||||
connections:
|
||||
# -- Change in case of bringing your own redis and then also set redis.enabled:false
|
||||
redis_host: "{{ .Release.Name }}-redis-headless"
|
||||
@@ -369,12 +369,6 @@ supersetNode:
|
||||
failureThreshold: 3
|
||||
periodSeconds: 15
|
||||
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.
|
||||
resources: {}
|
||||
# limits:
|
||||
@@ -415,7 +409,7 @@ supersetWorker:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec celery --app=superset.tasks.celery_app:app worker"
|
||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; celery --app=superset.tasks.celery_app:app worker"
|
||||
# -- If true, forces deployment to reload on each upgrade
|
||||
forceReload: false
|
||||
# -- Init container
|
||||
@@ -495,10 +489,6 @@ supersetWorker:
|
||||
failureThreshold: 3
|
||||
periodSeconds: 60
|
||||
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)
|
||||
startupProbe: {}
|
||||
# -- No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic)
|
||||
@@ -523,7 +513,7 @@ supersetCeleryBeat:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec celery --app=superset.tasks.celery_app:app beat --pidfile /tmp/celerybeat.pid --schedule /tmp/celerybeat-schedule"
|
||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; 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
|
||||
forceReload: false
|
||||
# -- List of init containers
|
||||
|
||||
@@ -375,6 +375,7 @@ select = [
|
||||
|
||||
ignore = [
|
||||
"S101",
|
||||
"PT004", # Fixtures that don't return values - underscore prefix conflicts with pytest usage
|
||||
"PT006",
|
||||
"T201",
|
||||
"N999",
|
||||
|
||||
46
superset-frontend/package-lock.json
generated
46
superset-frontend/package-lock.json
generated
@@ -36,7 +36,7 @@
|
||||
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
|
||||
"@jsonforms/core": "^3.7.0",
|
||||
"@jsonforms/react": "^3.7.0",
|
||||
"@jsonforms/vanilla-renderers": "^3.8.0",
|
||||
"@jsonforms/vanilla-renderers": "^3.7.0",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
"@luma.gl/engine": "~9.2.5",
|
||||
@@ -187,7 +187,7 @@
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.41",
|
||||
"@swc/plugin-emotion": "^14.13.0",
|
||||
"@swc/plugin-emotion": "^14.12.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
@@ -272,7 +272,7 @@
|
||||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.4.6",
|
||||
"storybook": "10.4.5",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
@@ -5387,41 +5387,41 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonforms/core": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonforms/core/-/core-3.8.0.tgz",
|
||||
"integrity": "sha512-XSvaZuQSs/MceG5nDDcrE879onPHkGBy0xEuLeZMUkSM/M8wc1dEUrJtMOZVNSITocm9YXFY1qQ5gnsPP38zAg==",
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonforms/core/-/core-3.7.0.tgz",
|
||||
"integrity": "sha512-CE9viWtwi9QWLqlWLeOul1/R1GRAyOA9y6OoUpsCc0FhyR+g5p29F3k0fUExHWxL0Sf4KHcXYkfhtqfRBPS8ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.3",
|
||||
"ajv": "^8.18.0",
|
||||
"ajv": "^8.6.1",
|
||||
"ajv-formats": "^2.1.0",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonforms/react": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonforms/react/-/react-3.8.0.tgz",
|
||||
"integrity": "sha512-k81+yWLpCQl+3XizS1bLjXoBwYhW1OAkjSXFA8W5qNtfPZjSOXDgtiuMOGYDv4b60tu2e9RB8h2P2O7QhfkhiA==",
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonforms/react/-/react-3.7.0.tgz",
|
||||
"integrity": "sha512-HkY7qAx8vW97wPEgZ7GxCB3iiXG1c95GuObxtcDHGPBJWMwnxWBnVYJmv5h7nthrInKsQKHZL5OusnC/sj/1GQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@jsonforms/core": "3.8.0",
|
||||
"@jsonforms/core": "3.7.0",
|
||||
"react": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonforms/vanilla-renderers": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonforms/vanilla-renderers/-/vanilla-renderers-3.8.0.tgz",
|
||||
"integrity": "sha512-s75TG4hSYgYLN9IRVhYtGjijqyhVXijgDhb2WnMqY+Ki7MQkLn9U7yg/l89NEpwzWS1sv0DxKUxriqVUq382og==",
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonforms/vanilla-renderers/-/vanilla-renderers-3.7.0.tgz",
|
||||
"integrity": "sha512-RdXQGsheARUJVbaTe6SqGw9W4/yrm0BgUok6OKUj8krp1NF4fqXc5UbYGHFksMR/p7LCuoYHCtQzKLXEfxJbDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@jsonforms/core": "3.8.0",
|
||||
"@jsonforms/react": "3.8.0",
|
||||
"@jsonforms/core": "3.7.0",
|
||||
"@jsonforms/react": "3.7.0",
|
||||
"react": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
@@ -10819,9 +10819,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/plugin-emotion": {
|
||||
"version": "14.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@swc/plugin-emotion/-/plugin-emotion-14.13.0.tgz",
|
||||
"integrity": "sha512-UT1l9tr934HjnktUiMGbw1rWrIXUhAByTB0DwZJwHmS8KWox+wNBIK4ZkJ2tKVU/PnQZRni+R9e6xklFkmgSYg==",
|
||||
"version": "14.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@swc/plugin-emotion/-/plugin-emotion-14.12.0.tgz",
|
||||
"integrity": "sha512-lyAQgTeDkowq/4+8JYaviVOL4jXSdObz+uuk84DjM0z4qoiMpI6xoDVp7/tjWeVjmLc2U6Qp3hDuwWMZ5xe88Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -39527,9 +39527,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.6.tgz",
|
||||
"integrity": "sha512-6wkA6LxfDSSilloITsrFOJfsnw0mDUP2h8Ls+lRt8oRsudtz2RWFhLv+Toiwg6NW7hUpdTDc2hzR7DztJid6+A==",
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.5.tgz",
|
||||
"integrity": "sha512-QZuv1gS9Tf9RMCjDw5JOfv1XSB5IhU0uhSKQNS7l/N9zDpmSydirCspkCNT9e0zkFfPkZ9vmQUTzHY/BA07saA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -39540,7 +39540,7 @@
|
||||
"@vitest/expect": "3.2.4",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@webcontainer/env": "^1.1.1",
|
||||
"esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0",
|
||||
"esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0",
|
||||
"open": "^10.2.0",
|
||||
"oxc-parser": "^0.127.0",
|
||||
"oxc-resolver": "^11.19.1",
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
|
||||
"@jsonforms/core": "^3.7.0",
|
||||
"@jsonforms/react": "^3.7.0",
|
||||
"@jsonforms/vanilla-renderers": "^3.8.0",
|
||||
"@jsonforms/vanilla-renderers": "^3.7.0",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
"@luma.gl/engine": "~9.2.5",
|
||||
@@ -270,7 +270,7 @@
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.41",
|
||||
"@swc/plugin-emotion": "^14.13.0",
|
||||
"@swc/plugin-emotion": "^14.12.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
@@ -355,7 +355,7 @@
|
||||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.4.6",
|
||||
"storybook": "10.4.5",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
|
||||
@@ -74,10 +74,7 @@ export function transformLinkUri(uri: string): string {
|
||||
// "java\tscript:" or "java\x01script:") are ignored by browsers, so strip
|
||||
// them before comparing against the blocklist.
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -519,8 +519,7 @@ const Select = forwardRef(
|
||||
handleSelectAll();
|
||||
}}
|
||||
>
|
||||
{t('Select all')}{' '}
|
||||
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
|
||||
{t('Select all')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -537,8 +536,7 @@ const Select = forwardRef(
|
||||
handleDeselectAll();
|
||||
}}
|
||||
>
|
||||
{t('Clear')}{' '}
|
||||
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
|
||||
{t('Clear')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
|
||||
</Button>
|
||||
</StyledBulkActionsContainer>
|
||||
),
|
||||
|
||||
@@ -97,11 +97,8 @@ testWithAssets(
|
||||
});
|
||||
|
||||
// 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}/,
|
||||
{
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
},
|
||||
);
|
||||
await expect(panel.locator('li').first()).toHaveText(/\d{2}\.\d{2}\.\d{4}/, {
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -182,7 +182,10 @@ testWithAssets(
|
||||
// Now track POST /api/v1/chart/data requests around Clear All
|
||||
const postsAfterClearAll: string[] = [];
|
||||
const handler = (req: any) => {
|
||||
if (req.url().includes('/api/v1/chart/data') && req.method() === 'POST') {
|
||||
if (
|
||||
req.url().includes('/api/v1/chart/data') &&
|
||||
req.method() === 'POST'
|
||||
) {
|
||||
postsAfterClearAll.push(req.url());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -109,12 +109,7 @@ testWithAssets(
|
||||
id: chartLayoutKey,
|
||||
children: [],
|
||||
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 = {
|
||||
@@ -135,7 +130,9 @@ testWithAssets(
|
||||
defaultDataMask: {
|
||||
filterState: { value: [FILTER_VALUE] },
|
||||
extraFormData: {
|
||||
filters: [{ col: FILTER_COLUMN, op: 'IN', val: [FILTER_VALUE] }],
|
||||
filters: [
|
||||
{ col: FILTER_COLUMN, op: 'IN', val: [FILTER_VALUE] },
|
||||
],
|
||||
},
|
||||
},
|
||||
cascadeParentIds: [],
|
||||
@@ -161,14 +158,15 @@ testWithAssets(
|
||||
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
|
||||
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).
|
||||
const twoQueryPayloads: any[] = [];
|
||||
page.on('request', req => {
|
||||
if (req.url().includes('/api/v1/chart/data') && req.method() === 'POST') {
|
||||
if (
|
||||
req.url().includes('/api/v1/chart/data') &&
|
||||
req.method() === 'POST'
|
||||
) {
|
||||
try {
|
||||
const body = req.postDataJSON();
|
||||
if (body?.queries?.length === 2) {
|
||||
|
||||
@@ -50,10 +50,7 @@ import {
|
||||
getGuestToken,
|
||||
} from '../../helpers/api/embedded';
|
||||
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 { EmbeddedPage } from '../../pages/EmbeddedPage';
|
||||
import { EMBEDDED } from '../../utils/constants';
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
ControlPanelConfig,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
@@ -146,7 +145,7 @@ const config: ControlPanelConfig = {
|
||||
freeForm: true,
|
||||
label: t('Time Format'),
|
||||
renderTrigger: true,
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
default: 'smart_date',
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
|
||||
@@ -21,12 +21,10 @@
|
||||
import d3 from 'd3';
|
||||
import { extent as d3Extent } from 'd3-array';
|
||||
import {
|
||||
BinaryQueryObjectFilterClause,
|
||||
CategoricalColorNamespace,
|
||||
ContextMenuFilters,
|
||||
DataMask,
|
||||
ValueFormatter,
|
||||
getNumberFormatter,
|
||||
getSequentialSchemeRegistry,
|
||||
CategoricalColorNamespace,
|
||||
} from '@superset-ui/core';
|
||||
import countries, { countryOptions } from './countries';
|
||||
|
||||
@@ -67,28 +65,9 @@ interface CountryMapProps {
|
||||
formatter: ValueFormatter;
|
||||
colorScheme: string;
|
||||
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> = {};
|
||||
// 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) {
|
||||
const {
|
||||
@@ -96,15 +75,10 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
width,
|
||||
height,
|
||||
country,
|
||||
entity,
|
||||
linearColorScheme,
|
||||
formatter,
|
||||
colorScheme,
|
||||
sliceId,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
} = props;
|
||||
|
||||
const container = element;
|
||||
@@ -125,15 +99,7 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
? colorScale(d.country_id, sliceId)
|
||||
: (linearColorScale(d.metric) ?? '');
|
||||
});
|
||||
|
||||
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 colorFn = (d: GeoFeature) => colorMap[d.properties.ISO] || 'none';
|
||||
|
||||
const path = d3.geo.path();
|
||||
const div = d3.select(container);
|
||||
@@ -146,11 +112,6 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('preserveAspectRatio', 'xMidYMid meet');
|
||||
|
||||
// Only set grab cursor if not in edit mode
|
||||
if (!isEditMode) {
|
||||
svg.style('cursor', 'grab');
|
||||
}
|
||||
const backgroundRect = svg
|
||||
.append('rect')
|
||||
.attr('class', 'background')
|
||||
@@ -158,65 +119,40 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
.attr('height', height);
|
||||
const g = svg.append('g');
|
||||
const mapLayer = g.append('g').classed('map-layer', true);
|
||||
// Add hover popup for tooltip
|
||||
const hoverPopup = div.append('div').attr('class', 'hover-popup');
|
||||
|
||||
// Track mouse position to distinguish clicks from drags
|
||||
let mousedownPos: { x: number; y: number } | null = null;
|
||||
let centered: GeoFeature | null;
|
||||
|
||||
// Cross-filter support
|
||||
const getCrossFilterDataMask = (
|
||||
source: GeoFeature,
|
||||
): { dataMask: DataMask; isCurrentValueSelected: boolean } | undefined => {
|
||||
if (!entity) return undefined;
|
||||
const clicked = function clicked(d: GeoFeature) {
|
||||
const hasCenter = d && centered !== d;
|
||||
let x: number;
|
||||
let y: number;
|
||||
let k: number;
|
||||
const halfWidth = width / 2;
|
||||
const halfHeight = height / 2;
|
||||
|
||||
const selected = filterState?.selectedValues || [];
|
||||
const iso = source?.properties?.ISO;
|
||||
if (!iso) return undefined;
|
||||
|
||||
const isSelected = selected.includes(iso);
|
||||
const values = isSelected ? [] : [iso];
|
||||
|
||||
return {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: values.length
|
||||
? [{ col: entity, op: 'IN', val: values }]
|
||||
: [],
|
||||
},
|
||||
filterState: {
|
||||
value: values.length ? values : null,
|
||||
selectedValues: values.length ? values : null,
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: isSelected,
|
||||
};
|
||||
};
|
||||
|
||||
// Handle right-click context menu
|
||||
const handleContextMenu = (feature: GeoFeature): void => {
|
||||
const pointerEvent = d3.event;
|
||||
|
||||
if (typeof onContextMenu === 'function') {
|
||||
pointerEvent?.preventDefault();
|
||||
if (hasCenter) {
|
||||
const centroid = path.centroid(d);
|
||||
[x, y] = centroid;
|
||||
k = 4;
|
||||
centered = d;
|
||||
} else {
|
||||
x = halfWidth;
|
||||
y = halfHeight;
|
||||
k = 1;
|
||||
centered = null;
|
||||
}
|
||||
|
||||
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' },
|
||||
});
|
||||
g.transition()
|
||||
.duration(750)
|
||||
.attr(
|
||||
'transform',
|
||||
`translate(${halfWidth},${halfHeight})scale(${k})translate(${-x},${-y})`,
|
||||
);
|
||||
};
|
||||
|
||||
backgroundRect.on('click', clicked);
|
||||
|
||||
const getNameOfRegion = function getNameOfRegion(
|
||||
feature: GeoFeature,
|
||||
): string {
|
||||
@@ -229,7 +165,7 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
return '';
|
||||
};
|
||||
|
||||
const updatePopupPosition = (): void => {
|
||||
const updatePopupPosition = () => {
|
||||
const svgHeight = svg.node().getBoundingClientRect().height;
|
||||
const [x, y] = d3.mouse(svg.node());
|
||||
hoverPopup
|
||||
@@ -239,135 +175,34 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
.classed('popup-at-bottom', y > (svgHeight * 2) / 3);
|
||||
};
|
||||
|
||||
const mouseenter = function mouseenter(
|
||||
this: SVGPathElement,
|
||||
d: GeoFeature,
|
||||
): void {
|
||||
const mouseenter = function mouseenter(this: SVGPathElement, d: GeoFeature) {
|
||||
// Darken color
|
||||
let c: string = colorFn(d);
|
||||
if (c) {
|
||||
if (c !== 'none') {
|
||||
c = d3.rgb(c).darker().toString();
|
||||
}
|
||||
d3.select(this).style('fill', c);
|
||||
|
||||
// Display information popup
|
||||
const result = data.filter(r => r.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(`<div><strong>${regionName}</strong><br>${metricValue}</div>`);
|
||||
const result = data.filter(
|
||||
region => region.country_id === d.properties.ISO,
|
||||
);
|
||||
|
||||
hoverPopup.style('display', 'block').html(
|
||||
`<div><strong>${getNameOfRegion(d)}</strong><br>${result.length > 0 ? formatter(result[0].metric) : ''}</div>`,
|
||||
);
|
||||
updatePopupPosition();
|
||||
};
|
||||
|
||||
// Mouse move handler to update tooltip position
|
||||
const mousemove = function mousemove(): void {
|
||||
const mousemove = function mousemove() {
|
||||
updatePopupPosition();
|
||||
};
|
||||
|
||||
const mouseout = function mouseout(this: SVGPathElement): void {
|
||||
d3.select(this).style('fill', (d: GeoFeature) => colorFn(d));
|
||||
const mouseout = function mouseout(this: SVGPathElement) {
|
||||
d3.select(this).style('fill', colorFn);
|
||||
hoverPopup.style('display', 'none');
|
||||
};
|
||||
|
||||
// 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 {
|
||||
function drawMap(mapData: GeoData) {
|
||||
const { features } = mapData;
|
||||
const center = d3.geo.centroid(mapData);
|
||||
const scale = 100;
|
||||
@@ -378,11 +213,13 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
.translate([width / 2, height / 2]);
|
||||
path.projection(projection);
|
||||
|
||||
// Compute scale that fits container.
|
||||
const bounds = path.bounds(mapData);
|
||||
const hscale = (scale * width) / (bounds[1][0] - bounds[0][0]);
|
||||
const vscale = (scale * height) / (bounds[1][1] - bounds[0][1]);
|
||||
const newScale = Math.min(hscale, vscale);
|
||||
const newScale = hscale < vscale ? hscale : vscale;
|
||||
|
||||
// Compute bounds and offset using the updated scale.
|
||||
projection.scale(newScale);
|
||||
const newBounds = path.bounds(mapData);
|
||||
projection.translate([
|
||||
@@ -390,45 +227,20 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
|
||||
height - (newBounds[0][1] + newBounds[1][1]) / 2,
|
||||
]);
|
||||
|
||||
const sel = mapLayer.selectAll('path.region').data(features);
|
||||
|
||||
sel
|
||||
// Draw each province as a path
|
||||
mapLayer
|
||||
.selectAll('path')
|
||||
.data(features)
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('class', 'region')
|
||||
.attr('vector-effect', 'non-scaling-stroke');
|
||||
|
||||
// Apply attributes and event handlers to all elements (enter + update)
|
||||
mapLayer
|
||||
.selectAll('path.region')
|
||||
.attr('d', path)
|
||||
.attr('class', 'region')
|
||||
.attr('vector-effect', 'non-scaling-stroke')
|
||||
.style('fill', colorFn)
|
||||
.on('mouseenter', mouseenter)
|
||||
.on('mousemove', mousemove)
|
||||
.on('mouseout', mouseout)
|
||||
.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();
|
||||
.on('click', clicked);
|
||||
}
|
||||
|
||||
const map = maps[country];
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import exampleUsa from './images/exampleUsa.jpg';
|
||||
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
||||
@@ -49,11 +49,6 @@ const metadata = new ChartMetadata({
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
behaviors: [
|
||||
Behavior.InteractiveChart,
|
||||
Behavior.DrillToDetail,
|
||||
Behavior.DrillBy,
|
||||
],
|
||||
});
|
||||
|
||||
export default class CountryMapChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -19,18 +19,8 @@
|
||||
import { ChartProps, getValueFormatter } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queriesData, datasource } = chartProps;
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
formData,
|
||||
queriesData,
|
||||
datasource,
|
||||
hooks = {},
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
} = chartProps;
|
||||
const {
|
||||
entity,
|
||||
linearColorScheme,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
@@ -59,8 +49,6 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
detectedCurrency,
|
||||
);
|
||||
|
||||
const { onContextMenu, setDataMask } = hooks;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
@@ -71,10 +59,5 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
colorScheme,
|
||||
sliceId,
|
||||
formatter,
|
||||
entity,
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
filterState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,11 +133,10 @@ describe('CountryMap (legacy d3)', () => {
|
||||
expect(popup!).toHaveStyle({ display: 'none' });
|
||||
});
|
||||
|
||||
test('emits a cross-filter data mask when a region is clicked', () => {
|
||||
test('shows tooltip on mouseenter/mousemove/mouseout', async () => {
|
||||
d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) =>
|
||||
cb(null, mockMapData),
|
||||
);
|
||||
const setDataMask = jest.fn();
|
||||
|
||||
render(
|
||||
<ReactCountryMap
|
||||
@@ -148,101 +147,19 @@ describe('CountryMap (legacy d3)', () => {
|
||||
linearColorScheme="bnbColors"
|
||||
colorScheme=""
|
||||
formatter={jest.fn().mockReturnValue('100')}
|
||||
entity="country_code"
|
||||
emitCrossFilters
|
||||
setDataMask={setDataMask}
|
||||
filterState={{ selectedValues: [] }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const region = document.querySelector('path.region');
|
||||
expect(region).not.toBeNull();
|
||||
|
||||
// A click is only treated as a selection when it follows a mousedown
|
||||
// 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!);
|
||||
const popup = document.querySelector('.hover-popup');
|
||||
expect(popup).not.toBeNull();
|
||||
|
||||
expect(setDataMask).toHaveBeenCalledTimes(1);
|
||||
expect(setDataMask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
extraFormData: {
|
||||
filters: [{ col: 'country_code', op: 'IN', val: ['CAN'] }],
|
||||
},
|
||||
filterState: expect.objectContaining({ value: ['CAN'] }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
fireEvent.mouseEnter(region!);
|
||||
expect(popup!).toHaveStyle({ display: 'block' });
|
||||
|
||||
test('does not emit a cross-filter when emitCrossFilters is disabled', () => {
|
||||
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',
|
||||
});
|
||||
fireEvent.mouseOut(region!);
|
||||
expect(popup!).toHaveStyle({ display: 'none' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,76 +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 { 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,7 +25,6 @@ import {
|
||||
D3_FORMAT_DOCS,
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import OptionDescription from './OptionDescription';
|
||||
@@ -155,7 +154,7 @@ const config: ControlPanelConfig = {
|
||||
freeForm: true,
|
||||
label: t('Date Time Format'),
|
||||
renderTrigger: true,
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
default: 'smart_date',
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
sections,
|
||||
getStandardizedControls,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -79,7 +78,7 @@ const config: ControlPanelConfig = {
|
||||
freeForm: true,
|
||||
label: t('Date Time Format'),
|
||||
renderTrigger: true,
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
default: 'smart_date',
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
|
||||
@@ -164,8 +164,7 @@ function WorldMap(element: HTMLElement, props: WorldMapProps): void {
|
||||
processedData = filteredData.map(d => ({
|
||||
...d,
|
||||
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,7 +26,6 @@ import {
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_FORMAT_OPTIONS,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
/*
|
||||
@@ -236,7 +235,7 @@ export const xAxisFormat: CustomControlItem = {
|
||||
label: t('X Axis Format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
default: 'smart_date',
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
const { cols: groupby, extra_form_data } = formData;
|
||||
const { cols: groupby } = formData;
|
||||
|
||||
const queryContextA = buildQueryContext(formData, baseQueryObject => {
|
||||
const postProcessing: PostProcessingRule[] = [];
|
||||
@@ -58,24 +58,14 @@ export default function buildQuery(formData: QueryFormData) {
|
||||
timeOffsets = timeOffsets.concat(['inherit']);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
extra_form_data?.time_compare &&
|
||||
!timeOffsets.includes(extra_form_data.time_compare)
|
||||
) {
|
||||
timeOffsets = [extra_form_data.time_compare];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
...baseQueryObject,
|
||||
groupby,
|
||||
post_processing: postProcessing,
|
||||
time_offsets:
|
||||
isTimeComparison(formData, baseQueryObject) ||
|
||||
extra_form_data?.time_compare
|
||||
? ensureIsArray(timeOffsets)
|
||||
: [],
|
||||
time_offsets: isTimeComparison(formData, baseQueryObject)
|
||||
? ensureIsArray(timeOffsets)
|
||||
: [],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -111,11 +111,7 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
const metrics = chartProps.datasource?.metrics || [];
|
||||
const originalLabel = getOriginalLabel(metric, metrics);
|
||||
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
||||
|
||||
const dashboardTimeCompare = formData?.extraFormData?.time_compare;
|
||||
const timeComparison =
|
||||
dashboardTimeCompare ||
|
||||
ensureIsArray(chartProps.rawFormData?.time_compare)[0];
|
||||
const timeComparison = ensureIsArray(chartProps.rawFormData?.time_compare)[0];
|
||||
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
||||
const currentTimeRangeFilter = chartProps.rawFormData?.adhoc_filters?.filter(
|
||||
(adhoc_filter: SimpleAdhocFilter) =>
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
ControlPanelState,
|
||||
getTemporalColumns,
|
||||
sharedControls,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -154,26 +153,12 @@ const config: ControlPanelConfig = {
|
||||
label: t('Date format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
default: 'smart_date',
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
['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,7 +74,6 @@ export default function transformProps(
|
||||
yAxisTitlePosition,
|
||||
sliceId,
|
||||
zoomable,
|
||||
yAxisSlider,
|
||||
} = formData as BoxPlotQueryFormData;
|
||||
const refs: Refs = {};
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
@@ -258,28 +257,6 @@ export default function transformProps(
|
||||
convertInteger(yAxisTitleMargin),
|
||||
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 = {
|
||||
grid: {
|
||||
...defaultGrid,
|
||||
@@ -321,7 +298,15 @@ export default function transformProps(
|
||||
},
|
||||
},
|
||||
},
|
||||
dataZoom,
|
||||
dataZoom: zoomable
|
||||
? [
|
||||
{
|
||||
type: 'inside',
|
||||
zoomOnMouseWheel: false,
|
||||
moveOnMouseWheel: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -30,7 +30,6 @@ export type BoxPlotQueryFormData = QueryFormData & {
|
||||
numberFormat?: string;
|
||||
whiskerOptions?: BoxPlotFormDataWhiskerOptions;
|
||||
xTickLayout?: BoxPlotFormXTickLayout;
|
||||
yAxisSlider?: boolean;
|
||||
} & TitleFormData;
|
||||
|
||||
export type BoxPlotFormDataWhiskerOptions =
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
getStandardizedControls,
|
||||
sharedControls,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
import { legendSection } from '../controls';
|
||||
@@ -189,7 +188,7 @@ const config: ControlPanelConfig = {
|
||||
label: t('Date format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
default: 'smart_date',
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
sharedControls,
|
||||
ControlFormItemSpec,
|
||||
getStandardizedControls,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
import { LabelPositionEnum } from '../types';
|
||||
@@ -182,7 +181,7 @@ const config: ControlPanelConfig = {
|
||||
label: t('Date format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
default: 'smart_date',
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
getStandardizedControls,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
|
||||
@@ -133,7 +132,7 @@ const config: ControlPanelConfig = {
|
||||
label: t('Date format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
default: 'smart_date',
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -182,6 +182,7 @@ const config: ControlPanelConfig = {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
checkColumnType(
|
||||
|
||||
@@ -174,6 +174,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
(isXAxis ? isVertical(controls) : isHorizontal(controls)) &&
|
||||
|
||||
@@ -147,6 +147,7 @@ const config: ControlPanelConfig = {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
checkColumnType(
|
||||
|
||||
@@ -113,6 +113,7 @@ const config: ControlPanelConfig = {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
checkColumnType(
|
||||
|
||||
@@ -112,6 +112,7 @@ const config: ControlPanelConfig = {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
checkColumnType(
|
||||
|
||||
@@ -164,6 +164,7 @@ const config: ControlPanelConfig = {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
checkColumnType(
|
||||
|
||||
@@ -258,6 +258,7 @@ export const tooltipTimeFormatControl: ControlSetItem = {
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
label: t('Tooltip time format'),
|
||||
default: 'smart_date',
|
||||
clearable: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,73 +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 { 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,15 +71,6 @@ describe('BoxPlot transformProps', () => {
|
||||
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', () => {
|
||||
expect(transformProps(chartProps as EchartsBoxPlotChartProps)).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -134,41 +125,4 @@ 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;
|
||||
}
|
||||
|
||||
export const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
formData: TableChartFormData,
|
||||
options,
|
||||
) => {
|
||||
@@ -217,17 +217,6 @@ export const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
|
||||
const moreProps: Partial<QueryObject> = {};
|
||||
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
|
||||
const isDownloadQuery =
|
||||
['csv', 'xlsx'].includes(formData?.result_format || '') ||
|
||||
@@ -240,24 +229,11 @@ export const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
}
|
||||
|
||||
if (!isDownloadQuery && formDataCopy.server_pagination) {
|
||||
// Never page past the configured row limit. Clamping the page to the last
|
||||
// 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;
|
||||
const pageSize = ownState.pageSize ?? formDataCopy.server_page_length;
|
||||
const currentPage = ownState.currentPage ?? 0;
|
||||
|
||||
moreProps.row_limit =
|
||||
configuredRowLimit > 0 ? Math.min(pageSize, remainingRows) : pageSize;
|
||||
moreProps.row_offset = rowOffset;
|
||||
moreProps.row_limit = pageSize;
|
||||
moreProps.row_offset = currentPage * pageSize;
|
||||
}
|
||||
|
||||
// getting sort by in case of server pagination from own state
|
||||
@@ -287,19 +263,11 @@ export const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
JSON.stringify(options?.extras?.cachedChanges?.[formData.slice_id]) !==
|
||||
JSON.stringify(queryObject.filters)
|
||||
) {
|
||||
// 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,
|
||||
};
|
||||
queryObject = { ...queryObject, row_offset: 0 };
|
||||
const modifiedOwnState = {
|
||||
...options?.ownState,
|
||||
currentPage: 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,
|
||||
pageSize: queryObject.row_limit ?? 0,
|
||||
};
|
||||
updateTableOwnState(options?.hooks?.setDataMask, modifiedOwnState);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { QueryMode, TimeGranularity, VizType } from '@superset-ui/core';
|
||||
import buildQuery, {
|
||||
buildQuery as buildQueryUncached,
|
||||
} from '../src/buildQuery';
|
||||
import buildQuery from '../src/buildQuery';
|
||||
import { TableChartFormData } from '../src/types';
|
||||
|
||||
const basicFormData: TableChartFormData = {
|
||||
@@ -280,172 +278,6 @@ describe('plugin-chart-table', () => {
|
||||
|
||||
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,10 +39,8 @@ jest.mock('@superset-ui/core/components/Select/AsyncSelect', () => () => (
|
||||
<div data-test="mock-async-select" />
|
||||
));
|
||||
jest.mock('src/core/editors', () => ({
|
||||
EditorHost: ({ value, height }: { value: string; height: string }) => (
|
||||
<div data-test="mock-async-ace-editor" data-height={height}>
|
||||
{value}
|
||||
</div>
|
||||
EditorHost: ({ value }: { value: string }) => (
|
||||
<div data-test="mock-async-ace-editor">{value}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -81,18 +79,6 @@ 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 () => {
|
||||
const { container, getByTestId } = setup();
|
||||
fireEvent.click(getByText(container, 'Parameters'));
|
||||
|
||||
@@ -30,9 +30,10 @@ import {
|
||||
import { EditorHost } from 'src/core/editors';
|
||||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||
|
||||
const EditorOutline = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.colorBorder};
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
const StyledEditorHost = styled(EditorHost)`
|
||||
&.ace_editor {
|
||||
border: 1px solid ${({ theme }) => theme.colorBorder};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledParagraph = styled.p`
|
||||
@@ -86,16 +87,14 @@ const TemplateParamsEditor = ({
|
||||
</a>{' '}
|
||||
{t('syntax.')}
|
||||
</StyledParagraph>
|
||||
<EditorOutline>
|
||||
<EditorHost
|
||||
id={`template-params-${queryEditorId}`}
|
||||
height="360px"
|
||||
onChange={debounce(onChange, Constants.FAST_DEBOUNCE)}
|
||||
language={language === 'yaml' ? 'yaml' : 'json'}
|
||||
width="100%"
|
||||
value={code}
|
||||
/>
|
||||
</EditorOutline>
|
||||
<StyledEditorHost
|
||||
id={`template-params-${queryEditorId}`}
|
||||
height="800px"
|
||||
onChange={debounce(onChange, Constants.FAST_DEBOUNCE)}
|
||||
language={language === 'yaml' ? 'yaml' : 'json'}
|
||||
width="100%"
|
||||
value={code}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -200,27 +200,6 @@ describe('sqlLabReducer', () => {
|
||||
expect(newState.unsavedQueryEditor.sql).toBe(sql);
|
||||
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', () => {
|
||||
const queryLimit = 101;
|
||||
const action = {
|
||||
|
||||
@@ -604,20 +604,8 @@ export default function sqlLabReducer(
|
||||
},
|
||||
[actions.QUERY_EDITOR_SET_SQL]() {
|
||||
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 (
|
||||
unsavedQueryEditor?.id === normalizedId &&
|
||||
unsavedQueryEditor?.id === action.queryEditor!.id &&
|
||||
unsavedQueryEditor.sql === action.sql
|
||||
) {
|
||||
return state;
|
||||
@@ -630,7 +618,7 @@ export default function sqlLabReducer(
|
||||
sql: action.sql ?? undefined,
|
||||
...(action.queryId && { latestQueryId: action.queryId }),
|
||||
},
|
||||
normalizedId,
|
||||
action.queryEditor!.id!,
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,160 +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 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,11 +18,8 @@
|
||||
*/
|
||||
import { AnyAction, Reducer } from 'redux';
|
||||
// eslint-disable-next-line import/named
|
||||
import undoable, {
|
||||
ActionCreators as UndoActionCreators,
|
||||
StateWithHistory,
|
||||
} from 'redux-undo';
|
||||
import { DASHBOARD_ROOT_ID, UNDO_LIMIT } from '../util/constants';
|
||||
import undoable, { StateWithHistory } from 'redux-undo';
|
||||
import { UNDO_LIMIT } from '../util/constants';
|
||||
import {
|
||||
UPDATE_COMPONENTS,
|
||||
DELETE_COMPONENT,
|
||||
@@ -100,7 +97,7 @@ const layoutOnlyReducer: Reducer<DashboardLayout, AnyAction> = (
|
||||
return dashboardLayout(state || {}, action);
|
||||
};
|
||||
|
||||
const baseUndoableReducer: Reducer<
|
||||
const undoableReducer: Reducer<
|
||||
StateWithHistory<DashboardLayout>,
|
||||
AnyAction
|
||||
> = undoable(layoutOnlyReducer, {
|
||||
@@ -110,53 +107,4 @@ const baseUndoableReducer: Reducer<
|
||||
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;
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
QueryFormData,
|
||||
DatasourceType,
|
||||
isDefined,
|
||||
isAdhocColumn,
|
||||
JsonValue,
|
||||
NO_TIME_RANGE,
|
||||
usePrevious,
|
||||
@@ -70,6 +71,7 @@ import {
|
||||
import Tabs from '@superset-ui/core/components/Tabs';
|
||||
import { PluginContext } from 'src/components';
|
||||
import { useConfirmModal } from 'src/hooks/useConfirmModal';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
|
||||
import { getSectionsToRender } from 'src/explore/controlUtils';
|
||||
import { ExploreActions } from 'src/explore/actions/exploreActions';
|
||||
@@ -300,6 +302,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
const theme = useTheme();
|
||||
const pluginContext = useContext(PluginContext);
|
||||
const { showConfirm, ConfirmModal } = useConfirmModal();
|
||||
const { addWarningToast } = useToasts();
|
||||
|
||||
const prevState = usePrevious(props.exploreState);
|
||||
const prevDatasource = usePrevious(props.exploreState.datasource);
|
||||
@@ -321,7 +324,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
|
||||
const { form_data, actions } = props;
|
||||
const { setControlValue } = actions;
|
||||
const { x_axis, adhoc_filters } = form_data;
|
||||
const { x_axis, adhoc_filters, granularity_sqla } = form_data;
|
||||
|
||||
const previousXAxis = usePrevious(x_axis);
|
||||
|
||||
@@ -370,6 +373,24 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
showConfirm,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
x_axis &&
|
||||
(previousXAxis === undefined || x_axis !== previousXAxis) &&
|
||||
isAdhocColumn(x_axis) &&
|
||||
!granularity_sqla
|
||||
) {
|
||||
addWarningToast(
|
||||
t(
|
||||
`The X-axis is a SQL expression, so time range filtering is not ` +
|
||||
`applied automatically. Without a time filter, queries may scan ` +
|
||||
`the entire table. Add a filter on your dataset's time column in ` +
|
||||
`the Filters section to limit the rows read.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [x_axis, previousXAxis, granularity_sqla, addWarningToast]);
|
||||
|
||||
useEffect(() => {
|
||||
let shouldUpdateControls = false;
|
||||
const removeDatasourceWarningFromControl = (
|
||||
|
||||
@@ -62,7 +62,6 @@ import { TagTypeEnum } from 'src/components/Tag/TagType';
|
||||
import { loadTags } from 'src/components/Tag/utils';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import copyTextToClipboard from 'src/utils/copy';
|
||||
import type Owner from 'src/types/Owner';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
import SavedQueryPreviewModal from 'src/features/queries/SavedQueryPreviewModal';
|
||||
import { findPermission } from 'src/utils/findPermission';
|
||||
@@ -92,15 +91,6 @@ interface SavedQueryListProps {
|
||||
};
|
||||
}
|
||||
|
||||
type SavedQueryCellProps = {
|
||||
row: {
|
||||
original: SavedQueryObject & {
|
||||
changed_by?: Owner | null;
|
||||
created_by?: Owner | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const StyledTableLabel = styled.div`
|
||||
.count {
|
||||
margin-left: 5px;
|
||||
@@ -445,30 +435,12 @@ function SavedQueryList({
|
||||
changed_on_delta_humanized: changedOn,
|
||||
},
|
||||
},
|
||||
}: SavedQueryCellProps) => (
|
||||
<ModifiedInfo user={changedBy ?? undefined} date={changedOn} />
|
||||
),
|
||||
}: any) => <ModifiedInfo user={changedBy} date={changedOn} />,
|
||||
Header: t('Last modified'),
|
||||
accessor: 'changed_on_delta_humanized',
|
||||
size: 'xl',
|
||||
id: 'changed_on_delta_humanized',
|
||||
},
|
||||
{
|
||||
accessor: 'created_by.first_name',
|
||||
Header: t('Created by'),
|
||||
disableSortBy: true,
|
||||
size: 'xl',
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { created_by: createdBy },
|
||||
},
|
||||
}: SavedQueryCellProps) =>
|
||||
createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
|
||||
},
|
||||
{
|
||||
accessor: 'created_by',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ row: { original } }: any) => {
|
||||
const handlePreview = () => {
|
||||
@@ -617,28 +589,6 @@ function SavedQueryList({
|
||||
),
|
||||
paginate: true,
|
||||
},
|
||||
{
|
||||
Header: t('Created by'),
|
||||
key: 'created_by',
|
||||
id: 'created_by',
|
||||
input: 'select',
|
||||
operator: FilterOperator.RelationOneMany,
|
||||
unfilteredLabel: t('All'),
|
||||
fetchSelects: createFetchRelated(
|
||||
'saved_query',
|
||||
'created_by',
|
||||
createErrorHandler(errMsg =>
|
||||
addDangerToast(
|
||||
t(
|
||||
'An error occurred while fetching created by values: %s',
|
||||
errMsg,
|
||||
),
|
||||
),
|
||||
),
|
||||
user,
|
||||
),
|
||||
paginate: true,
|
||||
},
|
||||
],
|
||||
[addDangerToast],
|
||||
);
|
||||
|
||||
1121
superset-websocket/package-lock.json
generated
1121
superset-websocket/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.61.1"
|
||||
"typescript-eslint": "^8.61.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.16.0",
|
||||
|
||||
@@ -66,9 +66,9 @@ def cidr_func(req: AdvancedDataTypeRequest) -> AdvancedDataTypeResponse:
|
||||
else:
|
||||
resp["display_value"] = ", ".join(
|
||||
map( # noqa: C417
|
||||
lambda x: (
|
||||
f"{x['start']} - {x['end']}" if isinstance(x, dict) else str(x)
|
||||
),
|
||||
lambda x: f"{x['start']} - {x['end']}"
|
||||
if isinstance(x, dict)
|
||||
else str(x),
|
||||
resp["values"],
|
||||
)
|
||||
)
|
||||
|
||||
@@ -95,9 +95,9 @@ def port_translation_func(req: AdvancedDataTypeRequest) -> AdvancedDataTypeRespo
|
||||
else:
|
||||
resp["display_value"] = ", ".join(
|
||||
map( # noqa: C417
|
||||
lambda x: (
|
||||
f"{x['start']} - {x['end']}" if isinstance(x, dict) else str(x)
|
||||
),
|
||||
lambda x: f"{x['start']} - {x['end']}"
|
||||
if isinstance(x, dict)
|
||||
else str(x),
|
||||
resp["values"],
|
||||
)
|
||||
)
|
||||
|
||||
@@ -402,6 +402,7 @@ class BaseReportState:
|
||||
merged_params = self._merge_native_filters_into_url_params(
|
||||
base_state.get("urlParams"), native_filter_params
|
||||
)
|
||||
|
||||
return [
|
||||
self._get_tab_url(
|
||||
{
|
||||
@@ -524,11 +525,7 @@ class BaseReportState:
|
||||
self._update_query_context()
|
||||
|
||||
try:
|
||||
csv_data = get_chart_csv_data(
|
||||
chart_url=url,
|
||||
auth_cookies=auth_cookies,
|
||||
timeout=app.config["ALERT_REPORTS_CSV_REQUEST_TIMEOUT"],
|
||||
)
|
||||
csv_data = get_chart_csv_data(chart_url=url, auth_cookies=auth_cookies)
|
||||
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
|
||||
logger.info(
|
||||
"CSV data generation from %s as user %s took %.2fs - execution_id: %s",
|
||||
@@ -578,11 +575,7 @@ class BaseReportState:
|
||||
self._update_query_context()
|
||||
|
||||
try:
|
||||
dataframe = get_chart_dataframe(
|
||||
url,
|
||||
auth_cookies,
|
||||
timeout=app.config["ALERT_REPORTS_CSV_REQUEST_TIMEOUT"],
|
||||
)
|
||||
dataframe = get_chart_dataframe(url, auth_cookies)
|
||||
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
|
||||
logger.info(
|
||||
"DataFrame generation from %s as user %s took %.2fs - execution_id: %s",
|
||||
|
||||
@@ -308,6 +308,29 @@ class QueryContextFactory: # pylint: disable=too-few-public-methods
|
||||
if filter["col"] != filter_to_remove
|
||||
]
|
||||
|
||||
elif is_adhoc_column(x_axis) and ( # type: ignore
|
||||
query_object.from_dttm or query_object.to_dttm
|
||||
):
|
||||
# x-axis is a SQL expression (not a physical temporal column) and a
|
||||
# time range is configured, but no time column (granularity) was set.
|
||||
# Fall back to the dataset's main datetime column so helpers.py adds a
|
||||
# WHERE time filter — without it engines like ClickHouse perform a full
|
||||
# table scan and trigger max_rows_to_read errors.
|
||||
main_dttm_col = getattr(datasource, "main_dttm_col", None)
|
||||
if main_dttm_col and main_dttm_col in temporal_columns:
|
||||
query_object.granularity = main_dttm_col
|
||||
# Remove any TEMPORAL_RANGE filter already targeting main_dttm_col
|
||||
# so helpers.py doesn't add the same condition a second time via the
|
||||
# filter loop (granularity covers it via from_dttm/to_dttm).
|
||||
query_object.filter = [
|
||||
f
|
||||
for f in query_object.filter
|
||||
if not (
|
||||
f.get("op") == "TEMPORAL_RANGE"
|
||||
and f.get("col") == main_dttm_col
|
||||
)
|
||||
]
|
||||
|
||||
def _apply_filters(self, query_object: QueryObject) -> None:
|
||||
if query_object.time_range:
|
||||
for filter_object in query_object.filter:
|
||||
|
||||
@@ -1153,12 +1153,6 @@ SCREENSHOT_LOCATE_WAIT = int(timedelta(seconds=10).total_seconds())
|
||||
# Time before selenium times out after waiting for all DOM class elements named
|
||||
# "loading" are gone.
|
||||
SCREENSHOT_LOAD_WAIT = int(timedelta(minutes=1).total_seconds())
|
||||
# Maximum time (in seconds) selenium waits for an initial page navigation
|
||||
# (driver.get) to complete. Without it the navigation blocks indefinitely when
|
||||
# the target page never finishes loading (e.g. an unreachable WEBDRIVER_BASEURL),
|
||||
# which leaves the report schedule stuck in the WORKING state. Set to None to
|
||||
# disable (not recommended).
|
||||
SCREENSHOT_PAGE_LOAD_WAIT = int(timedelta(minutes=2).total_seconds())
|
||||
# Selenium destroy retries
|
||||
SCREENSHOT_SELENIUM_RETRIES = 5
|
||||
# Give selenium an headstart, in seconds
|
||||
@@ -1742,11 +1736,6 @@ SMTP_MAIL_FROM = "superset@superset.com"
|
||||
# If True creates a default SSL context with ssl.Purpose.CLIENT_AUTH using the
|
||||
# default system root CA certificates.
|
||||
SMTP_SSL_SERVER_AUTH = False
|
||||
# Socket timeout (in seconds) for the SMTP connection used when sending
|
||||
# alert/report emails. Without a timeout the underlying socket blocks
|
||||
# indefinitely if the SMTP server becomes unreachable, which leaves report
|
||||
# schedules stuck in the WORKING state. Set to None to disable (not recommended).
|
||||
SMTP_TIMEOUT = 30
|
||||
ENABLE_CHUNK_ENCODING = False
|
||||
|
||||
# Whether to bump the logging level to ERROR on the flask_appbuilder package
|
||||
@@ -2095,12 +2084,6 @@ ALERT_REPORTS_NOTIFICATION_DRY_RUN = False
|
||||
# Max tries to run queries to prevent false errors caused by transient errors
|
||||
# being returned to users. Set to a value >1 to enable retries.
|
||||
ALERT_REPORTS_QUERY_EXECUTION_MAX_TRIES = 1
|
||||
# Socket timeout (in seconds) for the HTTP request that fetches chart data when
|
||||
# generating CSV/dataframe report attachments. Without a timeout the request
|
||||
# blocks indefinitely if the Superset webserver is unreachable from the worker,
|
||||
# which leaves the report schedule stuck in the WORKING state. Set to None to
|
||||
# disable (not recommended).
|
||||
ALERT_REPORTS_CSV_REQUEST_TIMEOUT = 60
|
||||
# Custom width for screenshots
|
||||
ALERT_REPORTS_MIN_CUSTOM_SCREENSHOT_WIDTH = 600
|
||||
ALERT_REPORTS_MAX_CUSTOM_SCREENSHOT_WIDTH = 2400
|
||||
@@ -2145,12 +2128,6 @@ SLACK_CACHE_TIMEOUT = int(timedelta(days=1).total_seconds())
|
||||
# For workspaces with 10k+ channels, consider increasing to 10
|
||||
SLACK_API_RATE_LIMIT_RETRY_COUNT = 2
|
||||
|
||||
# Timeout (in seconds) for outbound Slack API calls. The Slack SDK defaults to 30s;
|
||||
# exposing it here lets operators grant more time for large file uploads (multi-MB
|
||||
# CSVs, PDFs, screenshot sets) to congested or rate-limited Slack endpoints without
|
||||
# patching code, consistent with the SMTP/CSV/screenshot timeouts.
|
||||
SLACK_API_TIMEOUT = 30
|
||||
|
||||
# The webdriver to use for generating reports when using Selenium (not Playwright).
|
||||
# This setting is ignored when PLAYWRIGHT_REPORTS_AND_THUMBNAILS is enabled, as
|
||||
# Playwright always uses Chromium regardless of this value.
|
||||
|
||||
@@ -123,7 +123,7 @@ class DatetimeFormatDetector:
|
||||
sql = database.apply_limit_to_sql(sql, limit=self.sample_size, force=True)
|
||||
|
||||
# Execute query and get results
|
||||
df = database.get_df(sql, dataset.schema)
|
||||
df = database.get_df(sql, dataset.catalog, dataset.schema)
|
||||
|
||||
if df.empty or column.column_name not in df.columns:
|
||||
logger.warning(
|
||||
|
||||
@@ -294,20 +294,6 @@ class ImportV1MetricSchema(Schema):
|
||||
|
||||
return data
|
||||
|
||||
@pre_load
|
||||
def fix_template_params(
|
||||
self, data: dict[str, Any], **kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Fix for template_params initially being exported as an empty string.
|
||||
"""
|
||||
if (
|
||||
isinstance(data.get("template_params"), str)
|
||||
and data["template_params"].strip() == ""
|
||||
):
|
||||
data["template_params"] = None
|
||||
return data
|
||||
|
||||
metric_name = fields.String(required=True)
|
||||
verbose_name = fields.String(allow_none=True)
|
||||
metric_type = fields.String(allow_none=True)
|
||||
|
||||
@@ -31,12 +31,7 @@ from superset.exceptions import SupersetSecurityException
|
||||
from superset.extensions import cache_manager
|
||||
from superset.superset_typing import FlaskResponse
|
||||
from superset.utils import json
|
||||
from superset.utils.core import (
|
||||
apply_max_row_limit,
|
||||
DatasourceType,
|
||||
parse_boolean_string,
|
||||
SqlExpressionType,
|
||||
)
|
||||
from superset.utils.core import apply_max_row_limit, DatasourceType, SqlExpressionType
|
||||
from superset.views.base_api import BaseSupersetApi, statsd_metrics
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -56,9 +51,8 @@ class DatasourceRestApi(BaseSupersetApi):
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(
|
||||
action=lambda self, *args, **kwargs: (
|
||||
f"{self.__class__.__name__}.get_column_values"
|
||||
),
|
||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
|
||||
f".get_column_values",
|
||||
log_to_statsd=False,
|
||||
)
|
||||
def get_column_values(
|
||||
@@ -130,63 +124,13 @@ class DatasourceRestApi(BaseSupersetApi):
|
||||
|
||||
row_limit = apply_max_row_limit(app.config["FILTER_SELECT_ROW_LIMIT"])
|
||||
denormalize_column = not datasource.normalize_columns
|
||||
|
||||
# Cache distinct column-value results so a dashboard with many filters
|
||||
# backed by the same (often heavy) virtual dataset doesn't re-execute
|
||||
# the wrapping query per filter (#39342).
|
||||
#
|
||||
# Key fields:
|
||||
# - ``rls`` — full RLS fingerprint via
|
||||
# ``security_manager.get_rls_cache_key`` (the canonical helper used
|
||||
# by viz.py and query_context_processor.py). This is the sole
|
||||
# security-isolation field — two users with identical effective
|
||||
# RLS share a cache entry (intentional: they would see identical
|
||||
# filtered values anyway), while users with different RLS, guest
|
||||
# sessions with different guest-token RLS, and anonymous sessions
|
||||
# with no RLS each get their own partition. We deliberately do
|
||||
# NOT include the raw user id; doing so would defeat the
|
||||
# intended cross-user cache sharing without adding any real
|
||||
# security boundary beyond what the RLS fingerprint already
|
||||
# provides.
|
||||
# - ``changed_on`` — auto-busts cached entries when the dataset's
|
||||
# underlying SQL is edited.
|
||||
# - ``uid`` / ``col`` / ``limit`` / ``denorm`` — basic query-shape
|
||||
# isolation so different inputs never collide.
|
||||
force = parse_boolean_string(request.args.get("force"))
|
||||
cache_key = (
|
||||
"col_values:"
|
||||
+ hashlib.sha256(
|
||||
json.dumps(
|
||||
{
|
||||
"uid": datasource.uid,
|
||||
"col": column_name,
|
||||
"limit": row_limit,
|
||||
"denorm": denormalize_column,
|
||||
"rls": security_manager.get_rls_cache_key(datasource),
|
||||
"changed_on": str(getattr(datasource, "changed_on", "")),
|
||||
},
|
||||
sort_keys=True,
|
||||
).encode()
|
||||
).hexdigest()
|
||||
)
|
||||
|
||||
if (
|
||||
not force
|
||||
and (cached := cache_manager.data_cache.get(cache_key)) is not None
|
||||
):
|
||||
logger.debug(
|
||||
"column-values cache HIT: uid=%s col=%s", datasource.uid, column_name
|
||||
)
|
||||
response = self.response(200, result=cached)
|
||||
response.headers["X-Cache-Status"] = "HIT"
|
||||
return response
|
||||
|
||||
try:
|
||||
payload = datasource.values_for_column(
|
||||
column_name=column_name,
|
||||
limit=row_limit,
|
||||
denormalize_column=denormalize_column,
|
||||
)
|
||||
return self.response(200, result=payload)
|
||||
except KeyError:
|
||||
return self.response(
|
||||
400, message=f"Column name {column_name} does not exist"
|
||||
@@ -200,31 +144,6 @@ class DatasourceRestApi(BaseSupersetApi):
|
||||
),
|
||||
)
|
||||
|
||||
# Warn before caching very large payloads (high-cardinality columns)
|
||||
# so operators can spot cache-memory pressure before Redis OOMs.
|
||||
# Threshold is operator-tunable; defaults to 100k rows.
|
||||
warn_threshold = app.config.get("FILTER_VALUES_CACHE_WARN_THRESHOLD", 100_000)
|
||||
if (payload_size := len(payload)) > warn_threshold:
|
||||
logger.warning(
|
||||
"column-values payload exceeds cache-warn threshold: "
|
||||
"uid=%s col=%s rows=%d threshold=%d",
|
||||
datasource.uid,
|
||||
column_name,
|
||||
payload_size,
|
||||
warn_threshold,
|
||||
)
|
||||
|
||||
timeout = datasource.cache_timeout or app.config.get(
|
||||
"CACHE_DEFAULT_TIMEOUT", 300
|
||||
)
|
||||
cache_manager.data_cache.set(cache_key, payload, timeout=timeout)
|
||||
logger.debug(
|
||||
"column-values cache MISS: uid=%s col=%s", datasource.uid, column_name
|
||||
)
|
||||
response = self.response(200, result=payload)
|
||||
response.headers["X-Cache-Status"] = "MISS"
|
||||
return response
|
||||
|
||||
@expose(
|
||||
"/<datasource_type>/<int:datasource_id>/validate_expression/",
|
||||
methods=("POST",),
|
||||
@@ -233,9 +152,8 @@ class DatasourceRestApi(BaseSupersetApi):
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(
|
||||
action=lambda self, *args, **kwargs: (
|
||||
f"{self.__class__.__name__}.validate_expression"
|
||||
),
|
||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
|
||||
f".validate_expression",
|
||||
log_to_statsd=False,
|
||||
)
|
||||
def validate_expression(
|
||||
|
||||
@@ -636,7 +636,6 @@ class PrestoBaseEngineSpec(BaseEngineSpec, metaclass=ABCMeta):
|
||||
cls,
|
||||
database: Database,
|
||||
table: Table,
|
||||
indexes: list[dict[str, Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Returns the latest (max) partition value for a table
|
||||
@@ -661,15 +660,7 @@ class PrestoBaseEngineSpec(BaseEngineSpec, metaclass=ABCMeta):
|
||||
>>> latest_sub_partition('sub_partition_table', event_type='click')
|
||||
'2018-01-01'
|
||||
"""
|
||||
if indexes is None:
|
||||
indexes = database.get_indexes(table)
|
||||
|
||||
if not indexes:
|
||||
raise SupersetTemplateException(
|
||||
f"Error getting partition for {table}. "
|
||||
"Verify that this table has a partition."
|
||||
)
|
||||
|
||||
indexes = database.get_indexes(table)
|
||||
part_fields = indexes[0]["column_names"]
|
||||
for k in kwargs.keys(): # pylint: disable=consider-iterating-dictionary
|
||||
if k not in k in part_fields: # pylint: disable=comparison-with-itself
|
||||
|
||||
@@ -21,7 +21,6 @@ import logging
|
||||
import math
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import requests
|
||||
@@ -31,7 +30,7 @@ from sqlalchemy.engine.reflection import Inspector
|
||||
from sqlalchemy.engine.url import URL
|
||||
from sqlalchemy.exc import NoSuchTableError
|
||||
|
||||
from superset import cache_manager, db
|
||||
from superset import db
|
||||
from superset.common.db_query_status import QueryStatus
|
||||
from superset.constants import QUERY_CANCEL_KEY, QUERY_EARLY_CANCEL_KEY
|
||||
from superset.db_engine_specs.base import (
|
||||
@@ -72,18 +71,6 @@ class TrinoEngineSpec(PrestoBaseEngineSpec):
|
||||
engine_name = "Trino"
|
||||
allows_alias_to_source_column = False
|
||||
|
||||
# The full set of columns Trino's "<table>$partitions" exposes for an
|
||||
# Iceberg table. The real partition keys are nested in the "partition" ROW,
|
||||
# so none of these are user partition columns.
|
||||
iceberg_partitions_metadata_columns = frozenset(
|
||||
{"partition", "record_count", "file_count", "total_size", "data"}
|
||||
)
|
||||
# Always present for Iceberg; used as the positive signal so we don't act on
|
||||
# a table that merely happens to share one of the names above.
|
||||
iceberg_partitions_signature_columns = frozenset(
|
||||
{"record_count", "file_count", "total_size"}
|
||||
)
|
||||
|
||||
metadata = {
|
||||
"description": (
|
||||
"Trino is a distributed SQL query engine for big data analytics."
|
||||
@@ -179,37 +166,6 @@ class TrinoEngineSpec(PrestoBaseEngineSpec):
|
||||
and "error 401" in str(ex)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _filter_iceberg_partition_indexes(
|
||||
cls,
|
||||
indexes: list[dict[str, Any]] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Drop Iceberg "$partitions" metadata indexes.
|
||||
|
||||
A partition index is recognized as Iceberg metadata only when it carries
|
||||
the signature columns *and* every one of its columns is a known metadata
|
||||
field. Requiring the latter means an index with any real partition key
|
||||
(e.g. a Hive table partitioned on "ds" that also has a column named
|
||||
"record_count") is left untouched. Such an index has no real partition
|
||||
keys, so it's dropped entirely; all other indexes pass through unchanged.
|
||||
|
||||
:param indexes: the indexes associated with a table
|
||||
:returns: the indexes with Iceberg metadata indexes removed
|
||||
"""
|
||||
filtered_indexes = []
|
||||
for index in indexes or []:
|
||||
column_names = set(index.get("column_names") or [])
|
||||
is_iceberg_metadata = (
|
||||
index.get("name") == "partition"
|
||||
and cls.iceberg_partitions_signature_columns <= column_names
|
||||
and column_names <= cls.iceberg_partitions_metadata_columns
|
||||
)
|
||||
if not is_iceberg_metadata:
|
||||
filtered_indexes.append(index)
|
||||
|
||||
return filtered_indexes
|
||||
|
||||
@classmethod
|
||||
def get_extra_table_metadata(
|
||||
cls,
|
||||
@@ -218,9 +174,7 @@ class TrinoEngineSpec(PrestoBaseEngineSpec):
|
||||
) -> dict[str, Any]:
|
||||
metadata = {}
|
||||
|
||||
if indexes := cls._filter_iceberg_partition_indexes(
|
||||
database.get_indexes(table)
|
||||
):
|
||||
if indexes := database.get_indexes(table):
|
||||
col_names, latest_parts = cls.latest_partition(
|
||||
database,
|
||||
table,
|
||||
@@ -228,11 +182,8 @@ class TrinoEngineSpec(PrestoBaseEngineSpec):
|
||||
indexes=indexes,
|
||||
)
|
||||
|
||||
partition_values: Sequence[str | None]
|
||||
if latest_parts:
|
||||
partition_values = latest_parts
|
||||
else:
|
||||
partition_values = [None] * len(col_names)
|
||||
if not latest_parts:
|
||||
latest_parts = tuple([None] * len(col_names))
|
||||
|
||||
metadata["partitions"] = {
|
||||
"cols": sorted( # noqa: C414
|
||||
@@ -245,7 +196,7 @@ class TrinoEngineSpec(PrestoBaseEngineSpec):
|
||||
}
|
||||
)
|
||||
),
|
||||
"latest": dict(zip(col_names, partition_values, strict=False)),
|
||||
"latest": dict(zip(col_names, latest_parts, strict=False)),
|
||||
"partitionQuery": cls._partition_query(
|
||||
table=table,
|
||||
indexes=indexes,
|
||||
@@ -680,66 +631,3 @@ class TrinoEngineSpec(PrestoBaseEngineSpec):
|
||||
return super().get_indexes(database, inspector, table)
|
||||
except NoSuchTableError:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
@cache_manager.data_cache.memoize(timeout=60)
|
||||
def latest_partition(
|
||||
cls,
|
||||
database: Database,
|
||||
table: Table,
|
||||
show_first: bool = False,
|
||||
indexes: list[dict[str, Any]] | None = None,
|
||||
) -> tuple[list[str], list[str] | None]:
|
||||
"""
|
||||
Return the latest partition for a table.
|
||||
|
||||
Iceberg "$partitions" metadata fields are filtered out first, so we
|
||||
never build a latest-partition query against them. Memoized like the
|
||||
base implementation so the index lookup is not repeated on cache hits.
|
||||
|
||||
:param database: the database the query will be run against
|
||||
:param table: the table instance
|
||||
:param show_first: return the value for the first partitioning key when
|
||||
there are several
|
||||
:param indexes: the indexes associated with the table
|
||||
:returns: the column names and the latest partition values
|
||||
"""
|
||||
if indexes is None:
|
||||
indexes = database.get_indexes(table)
|
||||
|
||||
return super().latest_partition(
|
||||
database,
|
||||
table,
|
||||
show_first=show_first,
|
||||
indexes=cls._filter_iceberg_partition_indexes(indexes),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def latest_sub_partition(
|
||||
cls,
|
||||
database: Database,
|
||||
table: Table,
|
||||
indexes: list[dict[str, Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""
|
||||
Return the latest sub-partition value for a table.
|
||||
|
||||
Iceberg "$partitions" metadata fields are filtered out first, so the
|
||||
``latest_sub_partition`` macro never builds a query against them.
|
||||
|
||||
:param database: the database the query will be run against
|
||||
:param table: the table instance
|
||||
:param indexes: the indexes associated with the table
|
||||
:param kwargs: filtering criteria on the partition list
|
||||
:returns: the latest sub-partition value
|
||||
"""
|
||||
if indexes is None:
|
||||
indexes = database.get_indexes(table)
|
||||
|
||||
return super().latest_sub_partition(
|
||||
database,
|
||||
table,
|
||||
indexes=cls._filter_iceberg_partition_indexes(indexes),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user