Compare commits

..

3 Commits

Author SHA1 Message Date
Mehmet Salih Yavuz
45c8fef646 test: update query object to_dict snapshot for time_compare_full_range 2026-06-23 18:35:01 +03:00
Mehmet Salih Yavuz
0e4e9f231c fix(plugin-chart-echarts): keep main line continuous with full-range time shift
With the full-range option the offset series are outer-joined onto the
main series, inserting null rows wherever the comparison period has data
the current period lacks. The main (non-derived) series rendered with
connectNulls=false, so those interleaved nulls fragmented the line into
disconnected segments. Connect nulls for the main series too when the
option is on, matching the default left-join appearance while the
comparison line still extends across its full range.
2026-06-23 16:57:29 +03:00
Mehmet Salih Yavuz
8d5a6a9918 feat(explore): add full-range option for time-shift comparison
The default ECharts line chart left-joins each time-shifted comparison
series onto the current period's rows, so a historical series gets
truncated to wherever the main series ends (e.g. today's partial day).
The deprecated NVD3 line chart kept comparison series at their full
range, which some users relied on for intraday "today vs prior days"
overlays.

Add an opt-in "Show full range for time shift" control. When enabled,
offset series are outer-joined and their x-axis is rebuilt so each
comparison line spans its full period while the current-period line
stops at its last data point. Default off; existing behavior unchanged.

The option is wired only into the regular timeseries query builder, so
it is hidden on Mixed charts where it would be a no-op.
2026-06-23 12:44:34 +03:00
232 changed files with 1986 additions and 8757 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
@@ -43,7 +43,7 @@ jobs:
# the latest version. It's MIT: https://github.com/nbubna/store/blob/master/LICENSE-MIT
# pkg:npm/node-forge@1.3.1
# selecting BSD-3-Clause licensing terms for node-forge to ensure compatibility with Apache
allow-dependencies-licenses: pkg:npm/rgbcolor, pkg:npm/jszip@3.10.1
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/node-forge@1.3.1, pkg:npm/rgbcolor, pkg:npm/jszip@3.10.1
python-dependency-liccheck:
# NOTE: Configuration for liccheck lives in our pyproject.yml.
@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -32,12 +32,12 @@ 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
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: "temurin"
java-version: "11"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -18,12 +18,12 @@ 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
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: "temurin"
java-version: "11"

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -71,7 +71,7 @@ jobs:
node-version-file: "./docs/.nvmrc"
- name: Setup Python
uses: ./.github/actions/setup-backend/
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: "zulu"
java-version: "21"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,60 +0,0 @@
name: Sync requirements for Python dependency PRs
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: write
pull-requests: read
jobs:
sync-python-dep-requirements:
# This action is limited for (1) PRs authored by Dependabot and (2) upstream repo due to write back to remote
if: github.repository == 'apache/superset' && github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.head.repo.fork == false
runs-on: ubuntu-26.04
steps:
- name: Fetch Dependabot metadata
id: dependabot-metadata
shell: bash
env:
BRANCH_NAME: ${{ github.head_ref }}
run: |
# Get current branch name, extract the package ecosystem and return as GHA step output
packageEcosystem=$(echo "$BRANCH_NAME" | cut -d'/' -f2)
echo "package-ecosystem=$packageEcosystem" >> $GITHUB_OUTPUT
# 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
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: true
# Authenticate the Docker daemon so the python:slim pull in
# uv-pip-compile.sh uses our (much higher) authenticated rate limit
# instead of the shared-runner anonymous one.
- name: Login to Docker Hub
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
continue-on-error: true
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Sync requirements in containerized environment
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
run: ./scripts/uv-pip-compile.sh
- name: Push changes to remote PRs
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add requirements
git diff --cached --quiet && exit 0
git commit --signoff --message "build(deps): sync pinned requirements for Dependabot pip PRs"
git push origin "HEAD:refs/heads/${GITHUB_EVENT_PULL_REQUEST_HEAD_REF}"
env:
GITHUB_EVENT_PULL_REQUEST_HEAD_REF: ${{ github.event.pull_request.head.ref }}

View File

@@ -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

View File

@@ -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

View File

@@ -50,4 +50,3 @@ under the License.
- [4.1.4](./CHANGELOG/4.1.4.md)
- [5.0.0](./CHANGELOG/5.0.0.md)
- [6.0.0](./CHANGELOG/6.0.0.md)
- [6.1.0](./CHANGELOG/6.1.0.md)

File diff suppressed because it is too large Load Diff

View File

@@ -83,9 +83,6 @@ categories:
- name: Clark.de
url: https://clark.de/
- name: Cover Genius
url: https://covergenius.com/
- name: EnquiryLabs
url: https://www.enquirylabs.co.uk
@@ -95,10 +92,6 @@ categories:
- name: KarrotPay
url: https://www.daangnpay.com/
- name: NICE Actimize
url: https://www.niceactimize.com/
contributors: ["@stevensuting"]
- name: Remita
url: https://remita.net
contributors: ["@mujibishola"]
@@ -119,6 +112,9 @@ categories:
url: https://xendit.co/
contributors: ["@LieAlbertTriAdrian"]
- name: Cover Genius
url: https://covergenius.com/
Gaming:
- name: Popoko VM Games Studio
url: https://popoko.live
@@ -300,6 +296,7 @@ categories:
logo: hifadih.png
contributors: ["@saintLaurent00"]
# Logo approved by @anmol-hpe on behalf of HPE
- name: HPE
url: https://www.hpe.com/in/en/home.html
logo: hpe.png
@@ -432,10 +429,6 @@ categories:
logo: userguiding.svg
contributors: ["@tzercin"]
- name: Value Ad
url: https://bestpair.info/
contributors: ["@stevensuting"]
- name: Virtuoso QA
url: https://www.virtuosoqa.com
@@ -520,6 +513,10 @@ categories:
url: https://www.sunbird.org/
contributors: ["@eksteporg"]
- name: The GRAPH Network
url: https://thegraphnetwork.org/
contributors: ["@fccoelho"]
- name: Udemy
url: https://www.udemy.com/
contributors: ["@sungjuly"]
@@ -528,24 +525,7 @@ categories:
url: https://www.vipkid.com.cn/
contributors: ["@illpanda"]
Social Organization:
- name: Living Goods
url: https://www.livinggoods.org
contributors: ["@chelule"]
- name: One Acre Fund
url: https://oneacrefund.org/
contributors: ["@stevensuting"]
- name: Quest Alliance
url: https://www.questalliance.net/
contributors: ["@stevensuting"]
- name: The GRAPH Network
url: https://thegraphnetwork.org/
contributors: ["@fccoelho"]
- name: Wikimedia Foundation
- name: WikiMedia Foundation
url: https://wikimediafoundation.org
contributors: ["@vg"]
@@ -558,10 +538,6 @@ categories:
url: https://www.douroeci.com/
contributors: ["@nunohelibeires"]
- name: Rogow
url: https://rogow.com.br/
contributors: ["@nilmonto"]
- name: Safaricom
url: https://www.safaricom.co.ke/
contributors: ["@mmutiso"]
@@ -574,10 +550,11 @@ categories:
url: https://wattbewerb.de/
contributors: ["@wattbewerb"]
Healthcare:
- name: 2070Health
url: https://2070health.com/
- name: Rogow
url: https://rogow.com.br/
contributors: ["@nilmonto"]
Healthcare:
- name: Amino
url: https://amino.com
contributors: ["@shkr"]
@@ -590,6 +567,10 @@ categories:
url: https://www.getcare.io/
contributors: ["@alandao2021"]
- name: Living Goods
url: https://www.livinggoods.org
contributors: ["@chelule"]
- name: Maieutical Labs
url: https://maieuticallabs.it
contributors: ["@xrmx"]
@@ -608,10 +589,10 @@ categories:
- name: WeSure
url: https://www.wesure.cn/
HR / Staffing:
- name: bluquist
url: https://bluquist.com/
- name: 2070Health
url: https://2070health.com/
HR / Staffing:
- name: Swile
url: https://www.swile.co/
contributors: ["@PaoloTerzi"]
@@ -619,18 +600,21 @@ categories:
- name: Symmetrics
url: https://www.symmetrics.fyi
- name: bluquist
url: https://bluquist.com/
Government:
- name: City of Ann Arbor, MI
url: https://www.a2gov.org/
contributors: ["@sfirke"]
- name: NRLM - Sarathi, India
url: https://pib.gov.in/PressReleasePage.aspx?PRID=1999586
- name: RIS3 Strategy of CZ, MIT CR
url: https://www.ris3.cz/
contributors: ["@RIS3CZ"]
- name: NRLM - Sarathi, India
url: https://pib.gov.in/PressReleasePage.aspx?PRID=1999586
Mobile Software:
- name: VLMedia
url: https://www.vlmedia.com.tr

View File

@@ -227,9 +227,6 @@ Added a new combined datasource list endpoint at `GET /api/v1/datasource/` to se
- The endpoint is available to users with at least one of `can_read` on `Dataset` or `SemanticView`.
- Semantic views are included only when the `SEMANTIC_LAYERS` feature flag is enabled.
- The endpoint enforces strict `order_column` validation and returns `400` for invalid sort columns.
## 6.1.0
### ClickHouse minimum driver version bump
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.

View File

@@ -72,23 +72,20 @@ services:
- -c
- |
url="http://host.docker.internal:9000/static/assets/manifest.json"
max_attempts=300 # ~10 minutes at 2s intervals; first build can be slow
echo "Waiting for webpack dev server at $$url..."
max_attempts=150 # ~5 minutes at 2s intervals
echo "Waiting for webpack dev server at $url..."
attempt=0
until curl -sf --max-time 5 -H "Host: localhost" -o /dev/null "$$url"; do
attempt=$$((attempt + 1))
if [ "$$attempt" -ge "$$max_attempts" ]; then
echo "ERROR: webpack dev server did not serve $$url after $$max_attempts attempts." >&2
until curl -sf --max-time 5 -o /dev/null "$url"; do
attempt=$((attempt + 1))
if [ "$attempt" -ge "$max_attempts" ]; then
echo "ERROR: webpack dev server did not serve $url after $max_attempts attempts (~5 minutes)." >&2
echo "Is the dev server running? With BUILD_SUPERSET_FRONTEND_IN_DOCKER=false you must start it on the host (e.g. 'npm run dev' in superset-frontend)." >&2
exit 1
fi
if [ $$((attempt % 15)) -eq 0 ]; then
echo "Still waiting for webpack dev server... ($$attempt/$$max_attempts)"
fi
sleep 2
done
echo "Webpack dev server is ready; starting nginx."
exec /docker-entrypoint.sh nginx -g 'daemon off;'
exec nginx -g 'daemon off;'
redis:
image: redis:7

View File

@@ -81,19 +81,17 @@ case "${1}" in
app)
echo "Starting web app (using development server)..."
# Default to Flask debug mode in this dev compose entrypoint so the Talisman
# dev CSP (which permits 'unsafe-eval' required by React Refresh / HMR) is
# served. Operators can still set FLASK_DEBUG=false in docker/.env-local
# to exercise the production-like CSP and error handling.
: "${FLASK_DEBUG:=1}"
export FLASK_DEBUG
# Werkzeug's interactive debugger (/console) is a separate, security-sensitive
# feature and must be opted into explicitly via SUPERSET_DEBUG_ENABLED=true.
# Environment-based debugger control for security
# Only enable Werkzeug interactive debugger when explicitly requested
# Modern Werkzeug (3.0+) includes PIN protection, but defense-in-depth approach
# Override FLASK_DEBUG so the effective state matches SUPERSET_DEBUG_ENABLED even
# when FLASK_DEBUG=true is inherited from docker/.env or .flaskenv
if [[ "${SUPERSET_DEBUG_ENABLED:-}" == "true" ]]; then
export FLASK_DEBUG=1
DEBUGGER_FLAG="--debugger"
echo " ⚠️ Werkzeug debugger enabled (requires PIN for /console access)"
else
export FLASK_DEBUG=0
DEBUGGER_FLAG="--no-debugger"
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
fi

View File

@@ -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}" \

View File

@@ -28,19 +28,14 @@
# Skip builds when no docs changes (exit 0 = skip, non-zero = build).
# Checks for changes in docs/ and README.md (which gets pulled into docs).
#
# $CACHED_COMMIT_REF is the last *deployed* commit; it is set on incremental
# builds (notably the master production deploy) and empty on a context's
# first build (every deploy preview). The production path diffs against it
# and skips correctly.
#
# Deploy previews need different handling: Netlify checks out a *merge*
# commit, so $COMMIT_REF (the PR head SHA) is frequently not resolvable in
# the clone, and on a shallow clone `git merge-base` can fail too -- so the
# previous logic fell through to a build on every PR, even non-docs ones.
# Instead, always diff the checked-out HEAD against its merge-base with
# master, deepening the shallow clone until that merge-base resolves. If it
# genuinely can't be determined, exit non-zero to build (fail safe).
ignore = 'if [ -n "$CACHED_COMMIT_REF" ]; then git diff --quiet "$CACHED_COMMIT_REF" HEAD -- . ../README.md; else git fetch --no-tags origin master >/dev/null 2>&1 || true; i=0; while [ "$i" -lt 10 ] && ! git merge-base origin/master HEAD >/dev/null 2>&1; do git fetch --deepen=200 origin master >/dev/null 2>&1 || break; i=$((i+1)); done; BASE="$(git merge-base origin/master HEAD 2>/dev/null || true)"; if [ -z "$BASE" ]; then exit 1; fi; git diff --quiet "$BASE" HEAD -- . ../README.md; fi'
# $CACHED_COMMIT_REF is the last *deployed* commit. On a PR's first build it
# is empty, so the original `git diff` errored and Netlify fell back to
# building -- which is why every PR built a docs preview once even with no
# docs changes. When it is empty we instead diff the whole branch against its
# merge-base with master, so non-docs PRs are skipped from the very first
# build. Subsequent builds (and the master production build) keep the cheaper
# incremental $CACHED_COMMIT_REF diff. Any failure exits non-zero -> build.
ignore = 'if [ -n "$CACHED_COMMIT_REF" ]; then git diff --quiet "$CACHED_COMMIT_REF" "$COMMIT_REF" -- . ../README.md; else git fetch origin master --depth=100 >/dev/null 2>&1; git diff --quiet "$(git merge-base origin/master "$COMMIT_REF" 2>/dev/null || echo origin/master)" "$COMMIT_REF" -- . ../README.md; fi'
[build.environment]
# Node version matching docs/.nvmrc

View File

@@ -109,7 +109,7 @@
"globals": "^17.6.0",
"prettier": "^3.8.4",
"typescript": "~6.0.3",
"typescript-eslint": "^8.61.1",
"typescript-eslint": "^8.61.0",
"webpack": "^5.107.2"
},
"browserslist": {

View File

@@ -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

View File

@@ -4932,110 +4932,110 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.61.1", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz#6e4b7fee21f1983308e9e9b634ecbaf702c86006"
integrity sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==
"@typescript-eslint/eslint-plugin@8.61.0", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz#db20271974b94a3a54d3b9544e5f5b3481448400"
integrity sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.61.1"
"@typescript-eslint/type-utils" "8.61.1"
"@typescript-eslint/utils" "8.61.1"
"@typescript-eslint/visitor-keys" "8.61.1"
"@typescript-eslint/scope-manager" "8.61.0"
"@typescript-eslint/type-utils" "8.61.0"
"@typescript-eslint/utils" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.61.1", "@typescript-eslint/parser@^8.61.0":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.1.tgz#881fba60b50636249cdeea2e547bf75715254c72"
integrity sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==
"@typescript-eslint/parser@8.61.0", "@typescript-eslint/parser@^8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.0.tgz#1afe73c9ccce16b7a26d6b95f9400b0ccc34af87"
integrity sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==
dependencies:
"@typescript-eslint/scope-manager" "8.61.1"
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/typescript-estree" "8.61.1"
"@typescript-eslint/visitor-keys" "8.61.1"
"@typescript-eslint/scope-manager" "8.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
debug "^4.4.3"
"@typescript-eslint/project-service@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.1.tgz#fcd9739964a40867eed55f1ac318d3909f24b4af"
integrity sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==
"@typescript-eslint/project-service@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz#417a2feac32e8ebd336d63f068c3b42b736ea1ac"
integrity sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.61.1"
"@typescript-eslint/types" "^8.61.1"
"@typescript-eslint/tsconfig-utils" "^8.61.0"
"@typescript-eslint/types" "^8.61.0"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz#2479921a40fdb0afa18f5838fae6167264b417b2"
integrity sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==
"@typescript-eslint/scope-manager@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz#93c2520d05653fe65eb9ee98efc74fd0134a7852"
integrity sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==
dependencies:
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/visitor-keys" "8.61.1"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
"@typescript-eslint/tsconfig-utils@8.61.1":
"@typescript-eslint/tsconfig-utils@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba"
integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==
"@typescript-eslint/tsconfig-utils@^8.61.0":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz#ca88080e0cf191d49516d7f300b67aa090d2254f"
integrity sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==
"@typescript-eslint/tsconfig-utils@^8.61.1":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz#9440a673581c6d9de308c4d5803dd52ed5d71729"
integrity sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==
"@typescript-eslint/type-utils@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz#8fa18f453ee140893b47d339d1a6b64cac9b08a1"
integrity sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==
"@typescript-eslint/type-utils@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz#50219b57e6b89cecfb1a15f093b15ec9ee019974"
integrity sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==
dependencies:
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/typescript-estree" "8.61.1"
"@typescript-eslint/utils" "8.61.1"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/utils" "8.61.0"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.61.1":
"@typescript-eslint/types@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d"
integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==
"@typescript-eslint/types@^8.61.0":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.1.tgz#0c51f518e4e6848371a1c988e859d59eb7522d5a"
integrity sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==
"@typescript-eslint/types@^8.61.1":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.0.tgz#601427c10203d9f0f34f0b3e474df735eb12b593"
integrity sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==
"@typescript-eslint/typescript-estree@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz#febbe70365ac0bf7611262b61b338fc8797965c7"
integrity sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==
"@typescript-eslint/typescript-estree@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz#98ca47260bbf627fc28f018b3a0abf00e3090690"
integrity sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==
dependencies:
"@typescript-eslint/project-service" "8.61.1"
"@typescript-eslint/tsconfig-utils" "8.61.1"
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/visitor-keys" "8.61.1"
"@typescript-eslint/project-service" "8.61.0"
"@typescript-eslint/tsconfig-utils" "8.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.1.tgz#ffd1054de7dd33b7873cd6c6713ec6b0366316d3"
integrity sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==
"@typescript-eslint/utils@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f"
integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.61.1"
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/typescript-estree" "8.61.1"
"@typescript-eslint/scope-manager" "8.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/visitor-keys@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz#546cf102b4efdb72a9a08e63a1b0d7d745eb66eb"
integrity sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==
"@typescript-eslint/visitor-keys@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz#39b4e1ab8936d23bea973d39fd092f9aa21f275e"
integrity sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==
dependencies:
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/types" "8.61.0"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -8768,9 +8768,9 @@ http-parser-js@>=0.5.1:
integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==
http-proxy-middleware@^2.0.9:
version "2.0.10"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz#b2df7b705203d7a8c269ac8450cf96b00c532f94"
integrity sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==
version "2.0.9"
resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz"
integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==
dependencies:
"@types/http-proxy" "^1.17.8"
http-proxy "^1.18.1"
@@ -14502,15 +14502,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.61.1:
version "8.61.1"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.1.tgz#7c224a9a643b7f42d295c67a75c1e30fee8c3eaa"
integrity sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==
typescript-eslint@^8.61.0:
version "8.61.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz#6927fb94f5f29623e370d33fd9fa61f15d6d996b"
integrity sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==
dependencies:
"@typescript-eslint/eslint-plugin" "8.61.1"
"@typescript-eslint/parser" "8.61.1"
"@typescript-eslint/typescript-estree" "8.61.1"
"@typescript-eslint/utils" "8.61.1"
"@typescript-eslint/eslint-plugin" "8.61.0"
"@typescript-eslint/parser" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/utils" "8.61.0"
typescript@~6.0.3:
version "6.0.3"

View File

@@ -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.16.2 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.17.2](https://img.shields.io/badge/Version-0.17.2-informational?style=flat-square)
![Version: 0.16.2](https://img.shields.io/badge/Version-0.16.2-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -111,6 +111,9 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
| init.resources | object | `{}` | |
| init.tolerations | list | `[]` | |
| init.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to init job |
| initImage.pullPolicy | string | `"IfNotPresent"` | |
| initImage.repository | string | `"apache/superset"` | |
| initImage.tag | string | `"dockerize"` | |
| nameOverride | string | `nil` | Provide a name to override the name of the chart |
| nodeSelector | object | `{}` | |
| postgresql | object | see `values.yaml` | Configuration values for the postgresql dependency. ref: https://github.com/bitnami/charts/tree/main/bitnami/postgresql |
@@ -216,7 +219,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 +251,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 +314,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 +334,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 |

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -194,6 +194,11 @@ image:
imagePullSecrets: []
initImage:
repository: apache/superset
tag: dockerize
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8088
@@ -269,7 +274,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"
@@ -298,29 +303,15 @@ supersetNode:
# @default -- a container waiting for postgres
initContainers:
- name: wait-for-postgres
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
envFrom:
- secretRef:
name: "{{ tpl .Values.envFromSecret . }}"
command:
- /bin/bash
- /bin/sh
- -c
- |
# opening a /dev/tcp fd performs a TCP connect without sending any
# payload (avoids postgres "incomplete startup packet" log noise);
# no external `dockerize`, `nc`, or busybox needed. SECONDS-based
# deadline mirrors the prior `dockerize -timeout 120s` behaviour.
SECONDS=0
until (exec 3<>/dev/tcp/"$DB_HOST"/"$DB_PORT") 2>/dev/null; do
if [ "$SECONDS" -ge 120 ]; then
echo "timeout waiting for postgres at $DB_HOST:$DB_PORT after 120s" >&2
exit 1
fi
echo "waiting for postgres at $DB_HOST:$DB_PORT (elapsed ${SECONDS}s)"
sleep 2
done
echo "postgres at $DB_HOST:$DB_PORT is up"
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s
resources:
limits:
memory: "256Mi"
@@ -369,12 +360,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,38 +400,22 @@ 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
# @default -- a container waiting for postgres and redis
initContainers:
- name: wait-for-postgres-redis
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
envFrom:
- secretRef:
name: "{{ tpl .Values.envFromSecret . }}"
command:
- /bin/bash
- /bin/sh
- -c
- |
# See supersetNode.initContainers for the rationale.
SECONDS=0
wait_for() {
local host=$1 port=$2 name=$3
until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
if [ "$SECONDS" -ge 120 ]; then
echo "timeout waiting for $name at $host:$port after 120s" >&2
exit 1
fi
echo "waiting for $name at $host:$port (elapsed ${SECONDS}s)"
sleep 2
done
echo "$name at $host:$port is up"
}
wait_for "$DB_HOST" "$DB_PORT" postgres
wait_for "$REDIS_HOST" "$REDIS_PORT" redis
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
resources:
limits:
memory: "256Mi"
@@ -495,10 +464,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,38 +488,22 @@ 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
# @default -- a container waiting for postgres
initContainers:
- name: wait-for-postgres-redis
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
envFrom:
- secretRef:
name: "{{ tpl .Values.envFromSecret . }}"
command:
- /bin/bash
- /bin/sh
- -c
- |
# See supersetNode.initContainers for the rationale.
SECONDS=0
wait_for() {
local host=$1 port=$2 name=$3
until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
if [ "$SECONDS" -ge 120 ]; then
echo "timeout waiting for $name at $host:$port after 120s" >&2
exit 1
fi
echo "waiting for $name at $host:$port (elapsed ${SECONDS}s)"
sleep 2
done
echo "$name at $host:$port is up"
}
wait_for "$DB_HOST" "$DB_PORT" postgres
wait_for "$REDIS_HOST" "$REDIS_PORT" redis
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
resources:
limits:
memory: "256Mi"
@@ -645,31 +594,15 @@ supersetCeleryFlower:
# @default -- a container waiting for postgres and redis
initContainers:
- name: wait-for-postgres-redis
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
envFrom:
- secretRef:
name: "{{ tpl .Values.envFromSecret . }}"
command:
- /bin/bash
- /bin/sh
- -c
- |
# See supersetNode.initContainers for the rationale.
SECONDS=0
wait_for() {
local host=$1 port=$2 name=$3
until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
if [ "$SECONDS" -ge 120 ]; then
echo "timeout waiting for $name at $host:$port after 120s" >&2
exit 1
fi
echo "waiting for $name at $host:$port (elapsed ${SECONDS}s)"
sleep 2
done
echo "$name at $host:$port is up"
}
wait_for "$DB_HOST" "$DB_PORT" postgres
wait_for "$REDIS_HOST" "$REDIS_PORT" redis
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
resources:
limits:
memory: "256Mi"
@@ -831,26 +764,15 @@ init:
# @default -- a container waiting for postgres
initContainers:
- name: wait-for-postgres
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
envFrom:
- secretRef:
name: "{{ tpl .Values.envFromSecret . }}"
command:
- /bin/bash
- /bin/sh
- -c
- |
# See supersetNode.initContainers for the rationale.
SECONDS=0
until (exec 3<>/dev/tcp/"$DB_HOST"/"$DB_PORT") 2>/dev/null; do
if [ "$SECONDS" -ge 120 ]; then
echo "timeout waiting for postgres at $DB_HOST:$DB_PORT after 120s" >&2
exit 1
fi
echo "waiting for postgres at $DB_HOST:$DB_PORT (elapsed ${SECONDS}s)"
sleep 2
done
echo "postgres at $DB_HOST:$DB_PORT is up"
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s
resources:
limits:
memory: "256Mi"

View File

@@ -375,6 +375,7 @@ select = [
ignore = [
"S101",
"PT004", # Fixtures that don't return values - underscore prefix conflicts with pytest usage
"PT006",
"T201",
"N999",

View File

@@ -28,7 +28,7 @@ asyncio_mode = auto
filterwarnings =
ignore
always::sqlalchemy.exc.RemovedIn20Warning
error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
# error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
# error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning

View File

@@ -30,7 +30,7 @@ from flask import current_app
from flask_appbuilder import Model
from flask_migrate import downgrade, upgrade
from progress.bar import ChargingBar
from sqlalchemy import create_engine, inspect, text
from sqlalchemy import create_engine, inspect
from sqlalchemy.ext.automap import automap_base
from superset import db
@@ -154,7 +154,7 @@ def main( # noqa: C901
print(f"Migration goes from {down_revision} to {revision}")
current_revision = db.engine.execute(
text("SELECT version_num FROM alembic_version")
"SELECT version_num FROM alembic_version"
).scalar()
print(f"Current version of the DB is {current_revision}")

File diff suppressed because it is too large Load Diff

View File

@@ -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",
@@ -192,7 +192,7 @@
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^4.0.0",
"lodash": "^4.18.1",
"mapbox-gl": "^3.24.1",
"mapbox-gl": "^3.24.0",
"markdown-to-jsx": "^9.8.2",
"match-sorter": "^8.3.0",
"memoize-one": "^6.0.0",
@@ -203,7 +203,7 @@
"query-string": "9.4.0",
"re-resizable": "^6.11.2",
"react": "^18.3.0",
"react-arborist": "^3.10.5",
"react-arborist": "^3.10.1",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
@@ -260,17 +260,17 @@
"@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.15",
"@formatjs/intl-durationformat": "^0.10.14",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.61.0",
"@playwright/test": "^1.60.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.5",
"@storybook/addon-docs": "10.4.4",
"@storybook/addon-links": "10.4.4",
"@storybook/react-webpack5": "10.4.4",
"@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",
@@ -296,21 +296,21 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.61.1",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "^8.61.0",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"baseline-browser-mapping": "^2.10.37",
"baseline-browser-mapping": "^2.10.36",
"cheerio": "1.2.0",
"concurrently": "^10.0.3",
"copy-webpack-plugin": "^14.0.0",
"cross-env": "^10.1.0",
"css-loader": "^7.1.4",
"css-minimizer-webpack-plugin": "^8.0.0",
"eslint": "^10.5.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^4.4.5",
@@ -323,8 +323,8 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
"eslint-plugin-storybook": "10.4.5",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.0",
"eslint-plugin-storybook": "10.4.3",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -343,7 +343,7 @@
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
"open-cli": "^9.0.0",
"oxlint": "^1.70.0",
"oxlint": "^1.69.0",
"po2json": "^0.4.5",
"prettier": "3.8.4",
"prettier-plugin-packagejson": "^3.0.2",
@@ -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.4",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.1",

View File

@@ -37,7 +37,7 @@
"cross-env": "^10.1.0",
"fs-extra": "^11.3.5",
"jest": "^30.4.2",
"yeoman-test": "^11.6.0"
"yeoman-test": "^11.5.3"
},
"engines": {
"npm": ">= 4.0.0",

View File

@@ -132,6 +132,26 @@ export const advancedAnalyticsControls: ControlPanelSectionConfig = {
},
},
],
[
{
name: 'time_compare_full_range',
config: {
type: 'CheckboxControl',
label: t('Show full range for time shift'),
default: false,
description: t(
'Plot each time-shifted series across its full time range instead ' +
'of truncating it to the main series. Useful for comparing a ' +
'partial current period (e.g. today so far) against complete ' +
'prior periods (e.g. all of yesterday).',
),
visibility: ({ controls }) =>
Boolean(controls?.time_compare?.value) &&
(!Array.isArray(controls?.time_compare?.value) ||
controls.time_compare.value.length > 0),
},
},
],
[
{
name: 'comparison_type',

View File

@@ -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;
}

View File

@@ -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>
),

View File

@@ -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,
});
},
);

View File

@@ -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());
}
};

View File

@@ -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) {

View File

@@ -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';

View File

@@ -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,
},

View File

@@ -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];

View File

@@ -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 {

View File

@@ -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,
};
}

View File

@@ -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' });
});
});

View File

@@ -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();
});

View File

@@ -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,
},

View File

@@ -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,
},

View File

@@ -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,
}));
}

View File

@@ -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,
},
};

View File

@@ -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)
: [],
},
];
});

View File

@@ -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) =>

View File

@@ -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.',
),
},
},
],
],
},
],

View File

@@ -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 {

View File

@@ -30,7 +30,6 @@ export type BoxPlotQueryFormData = QueryFormData & {
numberFormat?: string;
whiskerOptions?: BoxPlotFormDataWhiskerOptions;
xTickLayout?: BoxPlotFormXTickLayout;
yAxisSlider?: boolean;
} & TitleFormData;
export type BoxPlotFormDataWhiskerOptions =

View File

@@ -318,14 +318,25 @@ function createAdvancedAnalyticsSection(
): ControlPanelSectionConfig {
const aaWithSuffix = cloneDeep(sections.advancedAnalyticsControls);
aaWithSuffix.label = label;
// `time_compare_full_range` is only wired into the regular timeseries query
// builder, not the mixed-timeseries one, so drop it here to avoid showing a
// control that has no effect.
aaWithSuffix.controlSetRows = aaWithSuffix.controlSetRows
.map(row =>
row.filter(
control =>
(control as CustomControlItem)?.name !== 'time_compare_full_range',
),
)
.filter(row => row.length > 0);
if (!controlSuffix) {
return aaWithSuffix;
}
aaWithSuffix.controlSetRows.forEach(row =>
row.forEach((control: CustomControlItem) => {
if (control?.name) {
// eslint-disable-next-line no-param-reassign
control.name = `${control.name}${controlSuffix}`;
row.forEach(control => {
const item = control as CustomControlItem;
if (item?.name) {
item.name = `${item.name}${controlSuffix}`;
}
}),
);

View File

@@ -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,
},
},

View File

@@ -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,
},
},

View File

@@ -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,
},
},

View File

@@ -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(

View File

@@ -396,102 +396,3 @@ test('does not emit cross-filter when no dimensions and time-based X-axis', asyn
expect(setDataMaskMock).not.toHaveBeenCalled();
}
});
// Test for issue #41102: horizontal bar cross-filter must use the category
// value, not the metric. For horizontal bars the data tuple is value-first
// (e.g. [100, 'Product A']), so relying on data[0] emitted the metric value.
test('emits cross-filter on the category value for a horizontal categorical bar', async () => {
const setDataMaskMock = jest.fn();
const propsWithHorizontalXAxis: TimeseriesChartTransformedProps = {
...defaultProps,
emitCrossFilters: true,
setDataMask: setDataMaskMock,
groupby: [], // No dimensions
xAxis: {
label: 'category_column',
type: AxisType.Category, // Categorical X-axis
},
};
render(<EchartsTimeseries {...propsWithHorizontalXAxis} />);
const lastCall = mockEchart.mock.calls.at(-1);
expect(lastCall).toBeDefined();
const [props] = lastCall as [EchartsProps];
const clickHandler = props.eventHandlers?.click;
if (clickHandler) {
clickHandler({
seriesName: 'Sales', // This is the metric name
data: [100, 'Product A'], // Horizontal: value first, category second
name: 'Product A',
dataIndex: 0,
});
await waitFor(
() => {
expect(setDataMaskMock).toHaveBeenCalled();
},
{ timeout: 500 },
);
// Must filter on the category ('Product A'), not the metric value (100)
const dataMaskCall = setDataMaskMock.mock.calls[0][0];
expect(dataMaskCall.extraFormData.filters).toEqual([
{
col: 'category_column',
op: 'IN',
val: ['Product A'],
},
]);
}
});
// Test for issue #41102: the context-menu ("Add cross-filter") path must also
// use the category value, not the metric, for a horizontal categorical bar.
test('context menu cross-filter uses the category value for a horizontal categorical bar', async () => {
const onContextMenuMock = jest.fn();
const propsWithHorizontalXAxis: TimeseriesChartTransformedProps = {
...defaultProps,
emitCrossFilters: true,
onContextMenu: onContextMenuMock,
groupby: [], // No dimensions
xAxis: {
label: 'category_column',
type: AxisType.Category, // Categorical X-axis
},
};
render(<EchartsTimeseries {...propsWithHorizontalXAxis} />);
const lastCall = mockEchart.mock.calls.at(-1);
expect(lastCall).toBeDefined();
const [props] = lastCall as [EchartsProps];
const contextMenuHandler = props.eventHandlers?.contextmenu;
expect(contextMenuHandler).toBeDefined();
if (contextMenuHandler) {
await contextMenuHandler({
seriesName: 'Sales', // This is the metric name
data: [100, 'Product A'], // Horizontal: value first, category second
name: 'Product A',
event: { stop: jest.fn(), event: { clientX: 10, clientY: 20 } },
});
await waitFor(() => {
expect(onContextMenuMock).toHaveBeenCalled();
});
// The cross-filter must use the category ('Product A'), not the metric (100)
const { crossFilter } = onContextMenuMock.mock.calls[0][2];
expect(crossFilter.dataMask.extraFormData.filters).toEqual([
{
col: 'category_column',
op: 'IN',
val: ['Product A'],
},
]);
}
});

View File

@@ -234,12 +234,9 @@ export default function EchartsTimeseries({
// Cross-filter by dimension (original behavior)
const { seriesName: name } = props;
handleChange(name);
} else if (canCrossFilterByXAxis && props.name != null) {
// Cross-filter by X-axis value when no dimensions (issue #25334).
// Use `name` (the category-axis value) instead of `data[0]`: for
// horizontal bars the data tuple is value-first, so `data[0]` would
// be the metric value rather than the category (issue #41102).
handleXAxisChange(props.name);
} else if (canCrossFilterByXAxis && props.data?.[0] != null) {
// Cross-filter by X-axis value when no dimensions (issue #25334)
handleXAxisChange(props.data[0]);
}
}, TIMER_DURATION);
},
@@ -321,10 +318,8 @@ export default function EchartsTimeseries({
let crossFilter;
if (hasDimensions) {
crossFilter = getCrossFilterDataMask(seriesName);
} else if (canCrossFilterByXAxis && eventParams.name != null) {
// Use `name` (the category-axis value), not `data[0]`, so horizontal
// bars cross-filter on the category and not the metric (issue #41102).
crossFilter = getXAxisCrossFilterDataMask(eventParams.name);
} else if (canCrossFilterByXAxis && data?.[0] != null) {
crossFilter = getXAxisCrossFilterDataMask(data[0]);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {

View File

@@ -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)) &&

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -82,6 +82,11 @@ export default function buildQuery(formData: QueryFormData) {
? formData.time_compare
: [];
// When comparing against prior periods, optionally keep each shifted series at
// its full time range instead of truncating it to the main series' range.
const time_compare_full_range =
time_offsets.length > 0 && Boolean(formData.time_compare_full_range);
return [
{
...baseQueryObject,
@@ -92,6 +97,7 @@ export default function buildQuery(formData: QueryFormData) {
// todo: move `normalizeOrderBy to extractQueryFields`
orderby: normalizeOrderBy(baseQueryObject).orderby,
time_offsets,
time_compare_full_range,
/* Note that:
1. The resample, rolling, cum, timeCompare operators should be after pivot.
2. Resample must come before rolling so that imputed values are

View File

@@ -381,6 +381,15 @@ export default function transformProps(
const array = ensureIsArray(chartProps.rawFormData?.time_compare);
const inverted = invert(verboseMap);
// With the "full range" time-shift option, offset series are outer-joined onto
// the main series, which inserts null rows into the main series wherever the
// comparison period has data the current period lacks. Connect nulls so the
// main line stays continuous (matching the default left-join appearance) rather
// than fragmenting at every inserted gap.
const timeCompareFullRange = Boolean(
chartProps.rawFormData?.time_compare_full_range,
);
const offsetLineWidths: { [key: string]: number } = {};
// For horizontal bar charts, calculate min/max from data to avoid cutting off labels
@@ -478,7 +487,7 @@ export default function transformProps(
colorScaleKey,
{
area,
connectNulls: derivedSeries,
connectNulls: derivedSeries || timeCompareFullRange,
filterState,
seriesContexts,
markerEnabled,

View File

@@ -258,6 +258,7 @@ export const tooltipTimeFormatControl: ControlSetItem = {
config: {
...sharedControls.x_axis_time_format,
label: t('Tooltip time format'),
default: 'smart_date',
clearable: false,
},
};

View File

@@ -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([]);
});
});

View File

@@ -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] }),
]),
);
});
});

View File

@@ -27,7 +27,7 @@
],
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.24.1",
"mapbox-gl": "^3.24.0",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.1",
"supercluster": "^8.0.1"

View File

@@ -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);
}

View File

@@ -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,
});
});
});
});
});

View File

@@ -20,6 +20,7 @@ import { css, styled, useTheme } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import type { NodeRendererProps } from 'react-arborist';
import { Icons, Typography } from '@superset-ui/core/components';
import RefreshLabel from '@superset-ui/core/components/RefreshLabel';
import ColumnElement from 'src/SqlLab/components/ColumnElement';
import { ActionButton } from '@superset-ui/core/components/ActionButton';
import copyTextToClipboard from 'src/utils/copy';
@@ -259,17 +260,16 @@ const TreeNodeRenderer: React.FC<TreeNodeRendererProps> = ({
</div>
)}
<div className="action-hover">
<ActionButton
label={`refresh-schema-${schema}`}
tooltip={t('Force refresh table list')}
icon={<Icons.SyncOutlined iconSize="m" />}
onClick={() => {
<RefreshLabel
onClick={e => {
e.stopPropagation();
handleRefreshTables({
dbId: Number(_dbId),
catalog,
schema,
});
}}
tooltipContent={t('Force refresh table list')}
/>
<ActionButton
label={

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