Compare commits

..

6 Commits

Author SHA1 Message Date
Evan
ed14e8b9d7 test(DropdownContainer): assert disabled state; clarify always-show comment
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:40:41 -07:00
Evan
4cef74b422 fix: remove unused recalculating state in DropdownContainer
The shouldShowButton approach superseded the recalculating-based
trigger gating, leaving the recalculating state and its setters
unused (TS6133), which broke lint-frontend and validate-frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:25:58 -07:00
Evan Rusackas
55d9e13725 address review: use Button tooltip prop so hint shows on disabled trigger
AntD disabled buttons swallow hover/focus, so the previous
<Tooltip><Button disabled /></Tooltip> wrapper left the
"No applied filters" hint unreachable in the exact state this PR
introduces (button visible, no popover content). The superset-ui Button
component's built-in `tooltip` prop already wraps disabled buttons in a
<span> so the tooltip fires — switch to that.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 20:15:22 -07:00
Evan Rusackas
ec13a2bb7b address review: guard empty popover and disable trigger when no content 2026-06-05 20:15:21 -07:00
Evan Rusackas
e274b45bc6 address review: timeless comment wording 2026-06-05 20:14:37 -07:00
Evan Rusackas
bd1420cfd8 fix(FilterBar): always show 'More filters' button when items exist
This fixes issue #28060 where the "More filters" button would disappear
when filter values were set to their defaults and nothing was overflowing.

The issue was caused by the button only rendering when `popoverContent`
was truthy, which required either `dropdownContent` to be defined OR
`overflowingCount > 0`. When neither condition was met (common when all
filters fit in the container), the button would vanish, causing:
- Inconsistent UI behavior
- Layout shifts as the button appears/disappears
- Poor user experience when resizing the browser window

The fix introduces a `shouldShowButton` flag that ensures the button is
always visible when items exist, regardless of overflow state. The badge
correctly shows 0 when nothing is overflowing, providing clear feedback
to users.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-05 20:14:36 -07:00
518 changed files with 15351 additions and 40713 deletions

View File

@@ -41,8 +41,8 @@ body:
label: Superset version label: Superset version
options: options:
- master / latest-dev - master / latest-dev
- "6.1.0"
- "6.0.0" - "6.0.0"
- "5.0.0"
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@@ -42,7 +42,7 @@ runs:
fi fi
echo "python-version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT" echo "python-version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Python ${{ steps.set-python-version.outputs.python-version }} - name: Set up Python ${{ steps.set-python-version.outputs.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with: with:
python-version: ${{ steps.set-python-version.outputs.python-version }} python-version: ${{ steps.set-python-version.outputs.python-version }}
cache: ${{ inputs.cache }} cache: ${{ inputs.cache }}

View File

@@ -14,6 +14,12 @@ updates:
- package-ecosystem: "npm" - package-ecosystem: "npm"
ignore: ignore:
# TODO: remove below entries until React >= 18.0.0
- dependency-name: "storybook"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "@storybook*"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "eslint-plugin-storybook"
- dependency-name: "react-error-boundary" - dependency-name: "react-error-boundary"
- dependency-name: "@rjsf/*" - dependency-name: "@rjsf/*"
# remark-gfm v4+ requires react-markdown v9+, which needs React 18 # remark-gfm v4+ requires react-markdown v9+, which needs React 18
@@ -36,6 +42,14 @@ updates:
# and confirm the issue https://github.com/apache/superset/issues/39600 is fixed # and confirm the issue https://github.com/apache/superset/issues/39600 is fixed
- dependency-name: "react-checkbox-tree" - dependency-name: "react-checkbox-tree"
update-types: ["version-update:semver-major"] update-types: ["version-update:semver-major"]
groups:
storybook:
applies-to: version-updates
patterns:
- "@storybook*"
- "storybook"
update-types:
- "patch"
directory: "/superset-frontend/" directory: "/superset-frontend/"
schedule: schedule:
interval: "daily" interval: "daily"
@@ -76,7 +90,21 @@ updates:
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/docs/" directory: "/docs/"
ignore: ignore:
# TODO: remove below entries until React >= 18.0.0 in superset-frontend
- dependency-name: "storybook"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "@storybook*"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "eslint-plugin-storybook"
- dependency-name: "react-error-boundary" - dependency-name: "react-error-boundary"
groups:
storybook:
applies-to: version-updates
patterns:
- "@storybook*"
- "storybook"
update-types:
- "patch"
schedule: schedule:
interval: "daily" interval: "daily"
open-pull-requests-limit: 10 open-pull-requests-limit: 10

View File

@@ -30,8 +30,9 @@ jobs:
pull-requests: write pull-requests: write
checks: write checks: write
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: true persist-credentials: true
ref: master ref: master

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: Check and notify - name: Check and notify

View File

@@ -75,14 +75,14 @@ jobs:
issues: write issues: write
id-token: write id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
fetch-depth: 1 fetch-depth: 1
- name: Run Claude PR Action - name: Run Claude PR Action
uses: anthropics/claude-code-action@5fb899572b81d2bb648d4d187173a2f423a9677c # beta uses: anthropics/claude-code-action@5fb899572b81d2bb648d4d187173a2f423a9677c # beta
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60" timeout_minutes: "60"

View File

@@ -26,7 +26,7 @@ jobs:
frontend: ${{ steps.check.outputs.frontend }} frontend: ${{ steps.check.outputs.frontend }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- name: Check for file changes - name: Check for file changes
@@ -57,13 +57,13 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -74,6 +74,6 @@ jobs:
# queries: security-extended,security-and-quality # queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: "Checkout Repository" - name: "Checkout Repository"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: "Dependency Review" - name: "Dependency Review"
@@ -51,7 +51,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: "Checkout Repository" - name: "Checkout Repository"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false

View File

@@ -18,6 +18,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
changes: changes:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
timeout-minutes: 10 timeout-minutes: 10
@@ -30,7 +31,7 @@ jobs:
docker: ${{ steps.check.outputs.docker }} docker: ${{ steps.check.outputs.docker }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: Check for file changes - name: Check for file changes
@@ -70,8 +71,9 @@ jobs:
IMAGE_TAG: apache/superset:GHA-${{ matrix.build_preset }}-${{ github.run_id }} IMAGE_TAG: apache/superset:GHA-${{ matrix.build_preset }}-${{ github.run_id }}
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
@@ -145,7 +147,7 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Docker Environment - name: Setup Docker Environment

View File

@@ -33,13 +33,13 @@ jobs:
run: run:
working-directory: superset-embedded-sdk working-directory: superset-embedded-sdk
steps: steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./superset-embedded-sdk/.nvmrc" node-version-file: './superset-embedded-sdk/.nvmrc'
registry-url: "https://registry.npmjs.org" registry-url: 'https://registry.npmjs.org'
- run: npm ci - run: npm ci
- run: npm run ci:release - run: npm run ci:release
env: env:

View File

@@ -21,13 +21,13 @@ jobs:
run: run:
working-directory: superset-embedded-sdk working-directory: superset-embedded-sdk
steps: steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./superset-embedded-sdk/.nvmrc" node-version-file: './superset-embedded-sdk/.nvmrc'
registry-url: "https://registry.npmjs.org" registry-url: 'https://registry.npmjs.org'
- run: npm ci - run: npm ci
- run: npm test - run: npm test
- run: npm run build - run: npm run build

View File

@@ -32,7 +32,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive

View File

@@ -18,6 +18,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
validate-all-ghas: validate-all-ghas:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions: permissions:
@@ -27,14 +28,14 @@ jobs:
security-events: write security-events: write
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: "20" node-version: '20'
- name: Install Dependencies - name: Install Dependencies
run: npm install -g @action-validator/core @action-validator/cli --save-dev run: npm install -g @action-validator/core @action-validator/cli --save-dev

View File

@@ -15,8 +15,9 @@ jobs:
pull-requests: write pull-requests: write
issues: write issues: write
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false

View File

@@ -11,29 +11,29 @@ jobs:
contents: write contents: write
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
- name: Check for latest tag - name: Check for latest tag
id: latest-tag id: latest-tag
env: env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
run: | run: |
source ./scripts/tag_latest_release.sh "$RELEASE_TAG_NAME" --dry-run source ./scripts/tag_latest_release.sh "$RELEASE_TAG_NAME" --dry-run
- name: Configure Git - name: Configure Git
run: | run: |
git config user.name "$GITHUB_ACTOR" git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com" git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Run latest-tag - name: Run latest-tag
uses: ./.github/actions/latest-tag uses: ./.github/actions/latest-tag
if: steps.latest-tag.outputs.SKIP_TAG != 'true' if: steps.latest-tag.outputs.SKIP_TAG != 'true'
with: with:
description: Superset latest release description: Superset latest release
tag-name: latest tag-name: latest
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}

View File

@@ -18,14 +18,14 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
- name: Setup Java - name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with: with:
distribution: "temurin" distribution: 'temurin'
java-version: "11" java-version: '11'
- name: Run license check - name: Run license check
run: ./scripts/check_license.sh run: ./scripts/check_license.sh

View File

@@ -21,7 +21,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -31,5 +31,6 @@ jobs:
on-failed-regex-fail-action: true on-failed-regex-fail-action: true
on-failed-regex-request-changes: false on-failed-regex-request-changes: false
on-failed-regex-create-review: false on-failed-regex-create-review: false
on-failed-regex-comment: "Please format your PR title to match: `%regex%`!" on-failed-regex-comment:
"Please format your PR title to match: `%regex%`!"
repo-token: "${{ github.token }}" repo-token: "${{ github.token }}"

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }} python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -48,9 +48,9 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "superset-frontend/.nvmrc" node-version: '20'
cache: "npm" cache: 'npm'
cache-dependency-path: "superset-frontend/package-lock.json" cache-dependency-path: 'superset-frontend/package-lock.json'
- name: Install Frontend Dependencies - name: Install Frontend Dependencies
run: | run: |
@@ -74,7 +74,7 @@ jobs:
id: changed_files id: changed_files
uses: ./.github/actions/file-changes-action uses: ./.github/actions/file-changes-action
with: with:
output: " " output: ' '
- name: pre-commit - name: pre-commit
env: env:

View File

@@ -33,7 +33,7 @@ jobs:
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
# pulls all commits (needed for lerna / semantic release to correctly version) # pulls all commits (needed for lerna / semantic release to correctly version)
@@ -52,7 +52,7 @@ jobs:
if: env.HAS_TAGS if: env.HAS_TAGS
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./superset-frontend/.nvmrc" node-version-file: './superset-frontend/.nvmrc'
- name: Cache npm - name: Cache npm
if: env.HAS_TAGS if: env.HAS_TAGS

View File

@@ -10,11 +10,11 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
pr_number: pr_number:
description: "PR number to sync" description: 'PR number to sync'
required: true required: true
type: number type: number
sha: sha:
description: "Specific SHA to deploy (optional, defaults to latest)" description: 'Specific SHA to deploy (optional, defaults to latest)'
required: false required: false
type: string type: string
@@ -152,7 +152,7 @@ jobs:
- name: Checkout PR code (only if build needed) - name: Checkout PR code (only if build needed)
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true' if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
ref: ${{ steps.check.outputs.target_sha }} ref: ${{ steps.check.outputs.target_sha }}
persist-credentials: false persist-credentials: false

View File

@@ -41,7 +41,7 @@ jobs:
- 16379:6379 - 16379:6379
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive

View File

@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}" - name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }} ref: ${{ github.event.workflow_run.head_sha || github.sha }}
persist-credentials: false persist-credentials: false
@@ -68,13 +68,13 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./docs/.nvmrc" node-version-file: './docs/.nvmrc'
- name: Setup Python - name: Setup Python
uses: ./.github/actions/setup-backend/ uses: ./.github/actions/setup-backend/
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with: with:
distribution: "zulu" distribution: 'zulu'
java-version: "21" java-version: '21'
- name: Install Graphviz - name: Install Graphviz
run: sudo apt-get install -y graphviz run: sudo apt-get install -y graphviz
- name: Compute Entity Relationship diagram (ERD) - name: Compute Entity Relationship diagram (ERD)

View File

@@ -28,12 +28,12 @@ jobs:
name: Link Checking name: Link Checking
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
# Do not bump this linkinator-action version without opening # Do not bump this linkinator-action version without opening
# an ASF Infra ticket to allow the new version first! # an ASF Infra ticket to allow the new version first!
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3 - uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
continue-on-error: true # This will make the job advisory (non-blocking, no red X) continue-on-error: true # This will make the job advisory (non-blocking, no red X)
with: with:
paths: "**/*.md, **/*.mdx" paths: "**/*.md, **/*.mdx"
@@ -73,14 +73,14 @@ jobs:
working-directory: docs working-directory: docs
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./docs/.nvmrc" node-version-file: './docs/.nvmrc'
- name: yarn install - name: yarn install
run: | run: |
yarn install --check-cache yarn install --check-cache
@@ -112,7 +112,7 @@ jobs:
working-directory: docs working-directory: docs
steps: steps:
- name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}" - name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
ref: ${{ github.event.workflow_run.head_sha }} ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false persist-credentials: false
@@ -120,7 +120,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./docs/.nvmrc" node-version-file: './docs/.nvmrc'
- name: yarn install - name: yarn install
run: | run: |
yarn install --check-cache yarn install --check-cache
@@ -131,7 +131,7 @@ jobs:
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
name: database-diagnostics name: database-diagnostics
path: docs/src/data/ path: docs/src/data/
if_no_artifact_found: "warning" if_no_artifact_found: 'warning'
- name: Use fresh diagnostics - name: Use fresh diagnostics
run: | run: |
if [ -f "src/data/databases-diagnostics.json" ]; then if [ -f "src/data/databases-diagnostics.json" ]; then

View File

@@ -10,17 +10,17 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
use_dashboard: use_dashboard:
description: "Use Cypress Dashboard (true/false) [paid service - trigger manually when needed]. You MUST provide a branch and/or PR number below for this to work." description: 'Use Cypress Dashboard (true/false) [paid service - trigger manually when needed]. You MUST provide a branch and/or PR number below for this to work.'
required: false required: false
default: "false" default: 'false'
ref: ref:
description: "The branch or tag to checkout" description: 'The branch or tag to checkout'
required: false required: false
default: "" default: ''
pr_id: pr_id:
description: "The pull request ID to checkout" description: 'The pull request ID to checkout'
required: false required: false
default: "" default: ''
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
@@ -38,7 +38,7 @@ jobs:
frontend: ${{ steps.check.outputs.frontend }} frontend: ${{ steps.check.outputs.frontend }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: Check for file changes - name: Check for file changes
@@ -97,21 +97,21 @@ jobs:
# Conditional checkout based on context # Conditional checkout based on context
- name: Checkout for push or pull_request event - name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request' if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch) - name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != '' if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
ref: ${{ github.event.inputs.ref }} ref: ${{ github.event.inputs.ref }}
submodules: recursive submodules: recursive
- name: Checkout using PR ID (workflow_dispatch) - name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != '' if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
@@ -130,9 +130,9 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./superset-frontend/.nvmrc" node-version-file: './superset-frontend/.nvmrc'
cache: "npm" cache: 'npm'
cache-dependency-path: "superset-frontend/package-lock.json" cache-dependency-path: 'superset-frontend/package-lock.json'
- name: Install npm dependencies - name: Install npm dependencies
uses: ./.github/actions/cached-dependencies uses: ./.github/actions/cached-dependencies
with: with:
@@ -207,21 +207,21 @@ jobs:
# Conditional checkout based on context (same as Cypress workflow) # Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event - name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request' if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch) - name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != '' if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
ref: ${{ github.event.inputs.ref }} ref: ${{ github.event.inputs.ref }}
submodules: recursive submodules: recursive
- name: Checkout using PR ID (workflow_dispatch) - name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != '' if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
@@ -240,9 +240,9 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./superset-frontend/.nvmrc" node-version-file: './superset-frontend/.nvmrc'
cache: "npm" cache: 'npm'
cache-dependency-path: "superset-frontend/package-lock.json" cache-dependency-path: 'superset-frontend/package-lock.json'
- name: Install npm dependencies - name: Install npm dependencies
uses: ./.github/actions/cached-dependencies uses: ./.github/actions/cached-dependencies
with: with:

View File

@@ -31,7 +31,7 @@ jobs:
working-directory: superset-extensions-cli working-directory: superset-extensions-cli
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -56,7 +56,7 @@ jobs:
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
if: steps.check.outputs.superset-extensions-cli if: steps.check.outputs.superset-extensions-cli
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: superset-extensions-cli flags: superset-extensions-cli

View File

@@ -27,7 +27,7 @@ jobs:
should-run: ${{ steps.check.outputs.frontend }} should-run: ${{ steps.check.outputs.frontend }}
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0
@@ -110,7 +110,7 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0
@@ -134,7 +134,7 @@ jobs:
run: npx nyc merge coverage/ merged-output/coverage-summary.json run: npx nyc merge coverage/ merged-output/coverage-summary.json
- name: Upload Code Coverage - name: Upload Code Coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with: with:
flags: javascript flags: javascript
use_oidc: true use_oidc: true

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -33,7 +33,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: ./.github/actions/setup-backend/ uses: ./.github/actions/setup-backend/
with: with:
install-superset: "false" install-superset: 'false'
- name: Set up chart-testing - name: Set up chart-testing
uses: ./.github/actions/chart-testing-action uses: ./.github/actions/chart-testing-action

View File

@@ -29,7 +29,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
ref: ${{ inputs.ref || github.ref_name }} ref: ${{ inputs.ref || github.ref_name }}
persist-credentials: true persist-credentials: true

View File

@@ -10,13 +10,13 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
ref: ref:
description: "The branch or tag to checkout" description: 'The branch or tag to checkout'
required: false required: false
default: "" default: ''
pr_id: pr_id:
description: "The pull request ID to checkout" description: 'The pull request ID to checkout'
required: false required: false
default: "" default: ''
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
@@ -34,7 +34,7 @@ jobs:
frontend: ${{ steps.check.outputs.frontend }} frontend: ${{ steps.check.outputs.frontend }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: Check for file changes - name: Check for file changes
@@ -83,21 +83,21 @@ jobs:
# Conditional checkout based on context (same as Cypress workflow) # Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event - name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request' if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch) - name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != '' if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
ref: ${{ github.event.inputs.ref }} ref: ${{ github.event.inputs.ref }}
submodules: recursive submodules: recursive
- name: Checkout using PR ID (workflow_dispatch) - name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != '' if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
@@ -116,9 +116,9 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./superset-frontend/.nvmrc" node-version-file: './superset-frontend/.nvmrc'
cache: "npm" cache: 'npm'
cache-dependency-path: "superset-frontend/package-lock.json" cache-dependency-path: 'superset-frontend/package-lock.json'
- name: Install npm dependencies - name: Install npm dependencies
uses: ./.github/actions/cached-dependencies uses: ./.github/actions/cached-dependencies
with: with:

View File

@@ -24,7 +24,7 @@ jobs:
python: ${{ steps.check.outputs.python }} python: ${{ steps.check.outputs.python }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: Check for file changes - name: Check for file changes
@@ -67,7 +67,7 @@ jobs:
- 16379:6379 - 16379:6379
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -85,7 +85,7 @@ jobs:
run: | run: |
./scripts/python_tests.sh ./scripts/python_tests.sh
- name: Upload code coverage - name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with: with:
flags: python,mysql flags: python,mysql
verbose: true verbose: true
@@ -152,7 +152,7 @@ jobs:
- 16379:6379 - 16379:6379
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -173,7 +173,7 @@ jobs:
run: | run: |
./scripts/python_tests.sh ./scripts/python_tests.sh
- name: Upload code coverage - name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with: with:
flags: python,postgres flags: python,postgres
verbose: true verbose: true
@@ -202,7 +202,7 @@ jobs:
- 16379:6379 - 16379:6379
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -222,7 +222,7 @@ jobs:
run: | run: |
./scripts/python_tests.sh ./scripts/python_tests.sh
- name: Upload code coverage - name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with: with:
flags: python,sqlite flags: python,sqlite
verbose: true verbose: true

View File

@@ -25,7 +25,7 @@ jobs:
python: ${{ steps.check.outputs.python }} python: ${{ steps.check.outputs.python }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: Check for file changes - name: Check for file changes
@@ -72,7 +72,7 @@ jobs:
- 16379:6379 - 16379:6379
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -90,7 +90,7 @@ jobs:
run: | run: |
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow' ./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
- name: Upload code coverage - name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with: with:
flags: python,presto flags: python,presto
verbose: true verbose: true
@@ -127,7 +127,7 @@ jobs:
- 16379:6379 - 16379:6379
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -152,7 +152,7 @@ jobs:
pip install -e .[hive] pip install -e .[hive]
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow' ./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
- name: Upload code coverage - name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with: with:
flags: python,hive flags: python,hive
verbose: true verbose: true

View File

@@ -25,7 +25,7 @@ jobs:
python: ${{ steps.check.outputs.python }} python: ${{ steps.check.outputs.python }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: Check for file changes - name: Check for file changes
@@ -50,7 +50,7 @@ jobs:
PYTHONPATH: ${{ github.workspace }} PYTHONPATH: ${{ github.workspace }}
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -72,7 +72,7 @@ jobs:
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100 pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100 pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
- name: Upload code coverage - name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with: with:
flags: python,unit flags: python,unit
verbose: true verbose: true

View File

@@ -25,7 +25,7 @@ jobs:
pull-requests: read pull-requests: read
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -40,9 +40,9 @@ jobs:
if: steps.check.outputs.frontend if: steps.check.outputs.frontend
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./superset-frontend/.nvmrc" node-version-file: './superset-frontend/.nvmrc'
cache: "npm" cache: 'npm'
cache-dependency-path: "superset-frontend/package-lock.json" cache-dependency-path: 'superset-frontend/package-lock.json'
- name: Install dependencies - name: Install dependencies
if: steps.check.outputs.frontend if: steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies uses: ./.github/actions/cached-dependencies
@@ -61,7 +61,7 @@ jobs:
pull-requests: read pull-requests: read
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: Install dependencies - name: Install dependencies

View File

@@ -9,7 +9,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
comment_body: comment_body:
description: "Comment Body" description: 'Comment Body'
required: true required: true
type: string type: string
@@ -38,7 +38,7 @@ jobs:
}); });
- name: "Checkout ( ${{ github.sha }} )" - name: "Checkout ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false

View File

@@ -16,11 +16,11 @@ on:
force-latest: force-latest:
required: true required: true
type: choice type: choice
default: "false" default: 'false'
description: Whether to force a latest tag on the release description: Whether to force a latest tag on the release
options: options:
- "true" - 'true'
- "false" - 'false'
permissions: permissions:
contents: read contents: read
@@ -49,12 +49,12 @@ jobs:
contents: write contents: write
strategy: strategy:
matrix: matrix:
build_preset: build_preset: ["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
fail-fast: false fail-fast: false
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0
@@ -119,8 +119,9 @@ jobs:
contents: read contents: read
pull-requests: write pull-requests: write
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0

View File

@@ -32,14 +32,14 @@ jobs:
name: Generate Reports name: Generate Reports
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: "./superset-frontend/.nvmrc" node-version-file: './superset-frontend/.nvmrc'
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci

View File

@@ -29,7 +29,7 @@ ARG BUILD_TRANSLATIONS="false"
###################################################################### ######################################################################
# superset-node-ci used as a base for building frontend assets and CI # superset-node-ci used as a base for building frontend assets and CI
###################################################################### ######################################################################
FROM --platform=${BUILDPLATFORM} node:24-trixie-slim AS superset-node-ci FROM --platform=${BUILDPLATFORM} node:22-trixie-slim AS superset-node-ci
ARG BUILD_TRANSLATIONS ARG BUILD_TRANSLATIONS
ENV BUILD_TRANSLATIONS=${BUILD_TRANSLATIONS} ENV BUILD_TRANSLATIONS=${BUILD_TRANSLATIONS}
ARG DEV_MODE="false" # Skip frontend build in dev mode ARG DEV_MODE="false" # Skip frontend build in dev mode
@@ -55,13 +55,6 @@ WORKDIR /app/superset-frontend
RUN mkdir -p /app/superset/static/assets \ RUN mkdir -p /app/superset/static/assets \
/app/superset/translations /app/superset/translations
# Harden `npm ci` against transient npm-registry network blips (e.g. ECONNRESET),
# which otherwise fail the entire multi-platform image build with no retry.
ENV npm_config_fetch_retries=5 \
npm_config_fetch_retry_mintimeout=20000 \
npm_config_fetch_retry_maxtimeout=120000 \
npm_config_fetch_timeout=600000
# Mount package files and install dependencies if not in dev mode # Mount package files and install dependencies if not in dev mode
# NOTE: we mount packages and plugins as they are referenced in package.json as workspaces # NOTE: we mount packages and plugins as they are referenced in package.json as workspaces
# ideally we'd COPY only their package.json. Here npm ci will be cached as long # ideally we'd COPY only their package.json. Here npm ci will be cached as long

View File

@@ -24,53 +24,6 @@ assists people when migrating to a new version.
## Next ## Next
### Webhook alerts/reports block private/internal hosts by default
Webhook alert/report dispatch (`WebhookNotification.send`) now validates the target URL's host against the same private/internal-IP block applied to dataset import URLs. If the resolved host is in a loopback, link-local, private (RFC-1918), shared-CGNAT, or multicast range, the webhook is rejected with `NotificationParamException`.
Deployments that intentionally point webhooks at internal targets (chatops bridges, internal automation servers, on-premises Mattermost/Rocket.Chat, etc.) can opt out by setting `ALERT_REPORTS_WEBHOOK_ALLOW_INTERNAL_HOSTS = True` in `superset_config.py`. This mirrors the existing `DATASET_IMPORT_ALLOW_INTERNAL_DATA_URLS` opt-out for dataset imports.
### Impala cancel_query blocks private/internal hosts by default
The Impala engine spec's `cancel_query` issues an HTTP request from the Superset backend to the host configured on the Impala database connection. That host is now validated before the request: if it resolves to a private/internal IP range, the cancel call is refused and a warning is logged. Operators whose Impala cluster runs on an internal network can opt out by setting `IMPALA_CANCEL_QUERY_ALLOW_INTERNAL_HOSTS = True` in `superset_config.py`. This mirrors the dataset-import and webhook opt-out flags.
### Map chart renderer and OpenStreetMap migration behavior
The MapLibre migration for deck.gl charts preserves saved non-Mapbox styles on
the MapLibre-compatible path. Saved styles such as OpenStreetMap, `tile://`
tile templates, generic HTTPS style URLs, and charts without a saved style are
not reclassified as Mapbox during migration and do not require
`MAPBOX_API_KEY` only because of the migration.
Saved true Mapbox styles whose value starts with `mapbox://` remain
Mapbox-backed. If a Superset deployment does not configure `MAPBOX_API_KEY`,
those saved Mapbox charts keep the existing missing-key message instead of
silently falling back to MapLibre or another provider. In Explore, deck.gl and
point-cluster renderer controls preserve saved Mapbox state, but the Mapbox
choice is not available as a new working renderer without a configured key.
The MapLibre style choices include `Streets (OSM)`, backed by
`https://tile.openstreetmap.org/{z}/{x}/{y}.png`. This OpenStreetMap tile
service requires visible `© OpenStreetMap contributors` attribution and should
be used through normal browser map tile requests and caching; it is not intended
for bulk prefetch or offline tile downloads.
### Password complexity policy enabled by default
Superset now ships a default password-complexity policy, enforced (via Flask-AppBuilder) across self-registration, the user create/edit/reset forms, and the User REST API. The policy requires a minimum password length of 8 characters and rejects a built-in blocklist of common/guessable passwords.
This is enabled by default (`FAB_PASSWORD_COMPLEXITY_ENABLED = True`), so new or reset passwords that are too short or appear in the blocklist will be rejected where they were previously accepted. Existing stored passwords are unaffected until they are next changed.
Operators can tune or disable the policy via config:
- `AUTH_PASSWORD_MIN_LENGTH` — minimum length (default `8`).
- `AUTH_PASSWORD_COMMON_BLOCKLIST` — extra passwords to reject, in addition to the built-in list.
- `FAB_PASSWORD_COMPLEXITY_VALIDATOR` — replace with your own callable for custom rules.
- `FAB_PASSWORD_COMPLEXITY_ENABLED = False` — disable enforcement entirely.
### Data uploads bounded by UPLOAD_MAX_FILE_SIZE_BYTES
Single data-file uploads (CSV, Excel, columnar) are now bounded by the `UPLOAD_MAX_FILE_SIZE_BYTES` config option, which defaults to `100 * 1024 * 1024` (100 MB). Files larger than this are rejected with a `413` before their contents are buffered into memory. Set `UPLOAD_MAX_FILE_SIZE_BYTES = None` to disable the check and restore unbounded uploads.
### Duration formatter precision ### Duration formatter precision
The `DURATION` number formatter now uses `Intl.DurationFormat` for locale-aware output. By default, sub-second fields are omitted, so values that previously displayed fractional seconds with `pretty-ms`, such as `10500` milliseconds rendering as `10.5s`, now render as `10s`. The `DURATION` number formatter now uses `Intl.DurationFormat` for locale-aware output. By default, sub-second fields are omitted, so values that previously displayed fractional seconds with `pretty-ms`, such as `10500` milliseconds rendering as `10.5s`, now render as `10s`.
@@ -91,36 +44,6 @@ The embedded dashboard page now validates the origin of incoming `postMessage` e
Enforcement only applies when the Allowed Domains list is non-empty. If the list is empty (the default), any origin is accepted, so there is no behavior change for embeds that did not configure Allowed Domains. Enforcement only applies when the Allowed Domains list is non-empty. If the list is empty (the default), any origin is accepted, so there is no behavior change for embeds that did not configure Allowed Domains.
### Default guest/async JWT secrets are rejected at startup
Superset already refuses to start in production (non-debug, non-testing) when `SECRET_KEY` is left at its built-in default, and when `GUEST_TOKEN_JWT_SECRET` is left at its default while `EMBEDDED_SUPERSET` is enabled. This behavior is extended to `GLOBAL_ASYNC_QUERIES_JWT_SECRET`: if the `GLOBAL_ASYNC_QUERIES` feature flag is enabled and the secret is still the publicly known default (`test-secret-change-me`), Superset logs a clear error and refuses to start.
As with the existing `SECRET_KEY` check, this only fails in production. In debug mode, testing mode, or under the test runner, a warning is logged instead of exiting, so local development is unaffected.
To resolve the error, set a strong random value in `superset_config.py`:
```python
GLOBAL_ASYNC_QUERIES_JWT_SECRET = "<output of: openssl rand -base64 42>"
```
The check is only active when the relevant feature is enabled, so deployments that do not use global async queries (or embedding) are not affected.
### Guest token revocation (opt-in)
Embedded guest tokens can be coarsely revoked at runtime via a new opt-in mechanism. A new config flag `GUEST_TOKEN_REVOCATION_ENABLED` (default `False`) gates the feature. When enabled, every minted guest token carries a revocation version, and tokens whose version is below the current expected version (stored in the metadata database) are rejected at validation time.
Bump the expected version with the new CLI command to invalidate all outstanding guest tokens:
```bash
superset revoke-guest-tokens
```
This change is backward compatible. The feature is off by default, and even when enabled nothing is revoked until an admin explicitly bumps the version: the expected version starts at `0`, and tokens minted before this change (which carry no version claim) are treated as version `0`. No database migration is required.
### Sessions are terminated when an account is disabled
Disabling a user account (setting `active` to `False`, via the admin UI, REST API, or CLI) now terminates that user's outstanding sessions on their next request, instead of relying on a passive check. This works for both client-side cookie sessions and server-side session stores via a per-user invalidation epoch (`user_attribute.sessions_invalidated_at`, added by a migration). The mechanism is inert for users that were never disabled (NULL epoch), so there is no behavior change for active users. Re-enabling an account and logging in again starts a fresh, valid session. The migration backfills the epoch for accounts that are already disabled at upgrade time, so re-enabling such an account does not revive a session that predates this feature.
### Dataset import validates catalog against the target connection ### Dataset import validates catalog against the target connection
Importing a dataset now validates the `catalog` field against the target database connection. When the connection has multi-catalog disabled (`allow_multi_catalog` off) and the dataset's catalog is not the connection's default catalog, the import fails instead of silently persisting the non-default catalog. This matches the validation already enforced on the dataset update path and prevents imported datasets from querying an unintended database. Importing a dataset now validates the `catalog` field against the target database connection. When the connection has multi-catalog disabled (`allow_multi_catalog` off) and the dataset's catalog is not the connection's default catalog, the import fails instead of silently persisting the non-default catalog. This matches the validation already enforced on the dataset update path and prevents imported datasets from querying an unintended database.
@@ -140,36 +63,6 @@ Both default to empty (no behavior change). They apply to both the `LOCAL_EXTENS
The Dynamic Group By chart customization now orders its display values according to the "Sort display control values" toggle: ascending (AZ), descending (ZA), or the dataset's source order when the toggle is unset. Previously the dropdown always sorted alphabetically. Existing dashboards where the toggle was never set will show options in source order instead of AZ; open the customization and enable the toggle to restore alphabetical ordering. The Dynamic Group By chart customization now orders its display values according to the "Sort display control values" toggle: ascending (AZ), descending (ZA), or the dataset's source order when the toggle is unset. Previously the dropdown always sorted alphabetically. Existing dashboards where the toggle was never set will show options in source order instead of AZ; open the customization and enable the toggle to restore alphabetical ordering.
### Selectable encryption engine for app-encrypted fields (AES-GCM)
App-encrypted fields (database passwords, SSH tunnel credentials, OAuth tokens, etc.) can now use authenticated **AES-GCM** encryption instead of the historical unauthenticated **AES-CBC**. A new config selects the engine for the default adapter:
```python
# "aes" (AES-CBC, historical default) | "aes-gcm" (authenticated, recommended for new installs)
SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes"
```
**No action required / no behavior change:** the default remains `"aes"`, so existing installs are unaffected.
**Opting in on an existing install:** flipping the engine on a populated database without re-encrypting first will make stored secrets undecryptable, because the two ciphertext formats are not compatible. A migrator is provided. Recommended runbook:
1. Take a metadata-DB backup.
2. Re-encrypt existing secrets into the new engine (the `SECRET_KEY` is unchanged):
```bash
superset re-encrypt-secrets --engine aes-gcm
```
3. Set `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes-gcm"` in your config.
4. Restart Superset.
5. Re-run the migrator once more after the restart:
```bash
superset re-encrypt-secrets --engine aes-gcm
```
A live instance keeps writing *new* secrets as AES-CBC during the window between step 2 and the restart in step 4; this second pass sweeps those up (it is idempotent, so already-migrated values are skipped).
Schedule the cutover in a quiet window. Runtime reads use only the single configured engine, so in a multi-worker deployment there is an unavoidable brief decrypt-outage between the migration commit and the last worker restarting with the new config — each migrator run is transactional, but the fleet-wide cutover is not zero-downtime.
The migration is transactional (all-or-nothing) and idempotent — it can be safely re-run or resumed. Note that AES-GCM, unlike AES-CBC, does not support querying directly over encrypted columns; audit any code that filters on an encrypted column before switching. See the SIP at `docs/sip/authenticated-encryption-at-rest.md` for details.
### Granular Export Controls ### Granular Export Controls
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission: A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:

View File

@@ -1 +1 @@
v24.16.0 v22.22.0

View File

@@ -72,11 +72,11 @@
"@superset-ui/core": "^0.20.4", "@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.40", "@swc/core": "^1.15.40",
"antd": "^6.4.3", "antd": "^6.4.3",
"baseline-browser-mapping": "^2.10.34", "baseline-browser-mapping": "^2.10.32",
"caniuse-lite": "^1.0.30001797", "caniuse-lite": "^1.0.30001793",
"docusaurus-plugin-openapi-docs": "^5.0.2", "docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2", "docusaurus-theme-openapi-docs": "^5.0.2",
"js-yaml": "^4.2.0", "js-yaml": "^4.1.1",
"js-yaml-loader": "^1.2.2", "js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"prism-react-renderer": "^2.4.1", "prism-react-renderer": "^2.4.1",
@@ -101,7 +101,7 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.59.3", "@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.60.1", "@typescript-eslint/parser": "^8.60.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6", "eslint-plugin-prettier": "^5.5.6",
@@ -109,7 +109,7 @@
"globals": "^17.6.0", "globals": "^17.6.0",
"prettier": "^3.8.3", "prettier": "^3.8.3",
"typescript": "~6.0.3", "typescript": "~6.0.3",
"typescript-eslint": "^8.60.1", "typescript-eslint": "^8.60.0",
"webpack": "^5.107.2" "webpack": "^5.107.2"
}, },
"browserslist": { "browserslist": {

View File

@@ -1,136 +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.
-->
# SIP: Authenticated encryption (AES-GCM) for app-encrypted fields
## [DRAFT — proposal for discussion]
This document is a draft proposal accompanying the code in this PR. It is
intended to seed the formal SIP discussion. The code here ships the
backward-compatible engine selection **and** the re-encryption migrator
(Phases 12 below); both are opt-in and change nothing for existing installs by
default. Flipping the default for fresh installs (Phase 3) remains future work.
## Motivation
Superset app-encrypts a number of sensitive fields before persisting them to
the metadata database, including:
- database connection passwords and `encrypted_extra` (`superset/models/core.py`),
- SSH tunnel credentials — password, private key, private-key password
(`superset/databases/ssh_tunnel/models.py`),
- OAuth2 tokens and other secrets stored via `EncryptedType`.
These fields are encrypted with `sqlalchemy_utils.EncryptedType`, which
**defaults to `AesEngine` (AES-CBC)**. AES-CBC provides confidentiality but is
**unauthenticated**: it has no integrity tag. An attacker with write access to
the ciphertext (e.g. direct metadata-DB access, a backup, or a compromised
replica) can perform **bit-flipping / chosen-ciphertext manipulation** to
silently alter the decrypted plaintext of a secret without detection.
`AesGcmEngine` (AES-GCM) is authenticated encryption: tampering causes
decryption to fail loudly rather than yielding attacker-influenced plaintext.
Using authenticated encryption for secrets at rest is an ASVS L1 expectation
(11.3.2 / cryptography best practice).
`config.py` already documents that operators *can* switch to GCM by writing a
custom `AbstractEncryptedFieldAdapter`, but:
1. it is opt-in, undocumented as a security recommendation, and easy to miss;
2. there is **no migration path** — flipping the engine on a populated database
makes every existing secret undecryptable, because GCM ciphertext is not
format-compatible with CBC.
## Proposed change
A three-part change, delivered incrementally so existing deployments are never
broken:
### Phase 1 — engine selection (this PR)
- Add a `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE` config (`"aes"` | `"aes-gcm"`),
**defaulting to `"aes"`** (no behavior change for existing installs).
- Teach the default `SQLAlchemyUtilsAdapter` to honor it (an explicit `engine`
kwarg still wins, so the migrator can pin an engine).
- This lets **new** deployments choose AES-GCM from day one with a one-line
config, instead of writing a custom adapter.
### Phase 2 — CBC→GCM re-encryption migrator (this PR)
The existing `SecretsMigrator` (previously only used for `SECRET_KEY` rotation)
gains an **engine migration** mode that:
1. discovers every `EncryptedType` column (via `discover_encrypted_fields()`),
2. decrypts each value with the **source** engine (AES-CBC) under the current
`SECRET_KEY`,
3. re-encrypts with the **target** engine (AES-GCM),
4. runs transactionally per the existing all-or-nothing semantics, and is
idempotent per column (already-migrated values are skipped), so a run can be
safely repeated or resumed.
Exposed via a new `--engine` option on the existing CLI command:
`superset re-encrypt-secrets --engine aes-gcm`, runnable by operators with a DB
backup in hand. The `SECRET_KEY` is unchanged; an engine change and a key
rotation can also be combined (pass `--previous_secret_key` as well).
### Phase 3 — flip the default for new installs
Once the migrator and docs are in place, change the default to `"aes-gcm"` for
**fresh** installs only (e.g. keyed off an empty metadata DB / documented in
`UPDATING.md`), keeping existing installs on `"aes"` until they run Phase 2.
## New or changed public interfaces
- New config: `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE: Literal["aes", "aes-gcm"]`.
- New (Phase 2) CLI: `superset re-encrypt-secrets --engine <name>`.
- No schema changes; ciphertext format changes per migrated column.
## Migration plan and compatibility
- **Backward compatible by default.** Phase 1 changes nothing unless the
operator opts in.
- Switching an existing deployment to `"aes-gcm"` **without** running the Phase
2 migrator will make existing secrets undecryptable — this is called out in
the config comment and must be in `UPDATING.md`.
- Recommended operator runbook: take a metadata-DB backup → run
`re-encrypt-secrets --engine aes-gcm` → set
`SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes-gcm"` → restart → re-run
`re-encrypt-secrets --engine aes-gcm` once more to sweep up any secrets a live
instance wrote as AES-CBC during the cutover window. The canonical, more
detailed version of this runbook lives in `UPDATING.md`; this is a summary.
- `AesEngine` allows queryability over encrypted fields; AES-GCM does not.
Any code that filters/queries on an encrypted column directly must be audited
before Phase 3 (none is expected, but it must be verified).
## Rejected alternatives
- **Flip the default immediately.** Rejected: bricks every existing
deployment's secrets with no migration path.
- **Document-only (custom adapter).** Status quo; high friction and no
migration tooling — most operators will never do it.
## Open questions
- GCM→CBC rollback (for operators who need queryability) already works via the
same command (`re-encrypt-secrets --engine aes`), since the migrator is
engine-symmetric. Should rollback be documented as a supported path or
discouraged?
- The migrator already supports a concurrent `SECRET_KEY` rotation + engine
change in a single pass (pass `--previous_secret_key` alongside `--engine`).
Is that combination worth calling out in the operator docs, or kept advanced?

View File

@@ -291,12 +291,6 @@ a > span > svg {
.footer__social-links img { .footer__social-links img {
height: 24px; height: 24px;
width: 24px; width: 24px;
/* The brand SVGs ship in their native colors (e.g. Slack's dark aubergine,
X's near-black), which disappear on the dark footer. Render them all as
uniform white silhouettes. The icons are single-path glyphs whose
counters (the LinkedIn "in", Slack gaps, Reddit face) are transparent
cut-outs, so they stay legible against the footer background. */
filter: brightness(0) invert(1);
} }
.footer__ci-services { .footer__ci-services {

View File

@@ -4812,110 +4812,110 @@
dependencies: dependencies:
"@types/yargs-parser" "*" "@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.60.1", "@typescript-eslint/eslint-plugin@^8.59.3": "@typescript-eslint/eslint-plugin@8.60.0", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.60.1" version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz#c1060bb8fa4be80624d3f3dec8dd9caca373af76" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz#8fc1e0a950c43270eaf0212dc060f7edaa42f9cf"
integrity sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg== integrity sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==
dependencies: dependencies:
"@eslint-community/regexpp" "^4.12.2" "@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.60.1" "@typescript-eslint/scope-manager" "8.60.0"
"@typescript-eslint/type-utils" "8.60.1" "@typescript-eslint/type-utils" "8.60.0"
"@typescript-eslint/utils" "8.60.1" "@typescript-eslint/utils" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.1" "@typescript-eslint/visitor-keys" "8.60.0"
ignore "^7.0.5" ignore "^7.0.5"
natural-compare "^1.4.0" natural-compare "^1.4.0"
ts-api-utils "^2.5.0" ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.60.1", "@typescript-eslint/parser@^8.60.1": "@typescript-eslint/parser@8.60.0", "@typescript-eslint/parser@^8.60.0":
version "8.60.1" version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.0.tgz#38d611b8e658cb10850d4975e8a175a222fbcd6a"
integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA== integrity sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==
dependencies: dependencies:
"@typescript-eslint/scope-manager" "8.60.1" "@typescript-eslint/scope-manager" "8.60.0"
"@typescript-eslint/types" "8.60.1" "@typescript-eslint/types" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.1" "@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.1" "@typescript-eslint/visitor-keys" "8.60.0"
debug "^4.4.3" debug "^4.4.3"
"@typescript-eslint/project-service@8.60.1": "@typescript-eslint/project-service@8.60.0":
version "8.60.1" version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz#eb29712f58d72c222fc727162e92f2ab4670971b" resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.0.tgz#b82ab12e64d005d0c7163d1240c432381f1bde0f"
integrity sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw== integrity sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==
dependencies: dependencies:
"@typescript-eslint/tsconfig-utils" "^8.60.1" "@typescript-eslint/tsconfig-utils" "^8.60.0"
"@typescript-eslint/types" "^8.60.1" "@typescript-eslint/types" "^8.60.0"
debug "^4.4.3" debug "^4.4.3"
"@typescript-eslint/scope-manager@8.60.1": "@typescript-eslint/scope-manager@8.60.0":
version "8.60.1" version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz#2f875962eaad0a0789cc3c36aea9b4ddeb2dd9c8" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz#7617a4617c043fe235dcf066f9a40f106cfd2fd5"
integrity sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w== integrity sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==
dependencies: dependencies:
"@typescript-eslint/types" "8.60.1" "@typescript-eslint/types" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.1" "@typescript-eslint/visitor-keys" "8.60.0"
"@typescript-eslint/tsconfig-utils@8.60.1": "@typescript-eslint/tsconfig-utils@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz#3af78c48956227a407dea9626b8db8ca53f130d2"
integrity sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==
"@typescript-eslint/tsconfig-utils@^8.60.0":
version "8.60.1" version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93" resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA== integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
"@typescript-eslint/tsconfig-utils@^8.60.1": "@typescript-eslint/type-utils@8.60.0":
version "8.61.0" version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz#6971a61bc4f3a1b2df45dcc14e26a43a88a4cb6a"
integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ== integrity sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==
"@typescript-eslint/type-utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz#1ae45f0f2a701354beea4a58c2161e40a5e3c379"
integrity sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==
dependencies: dependencies:
"@typescript-eslint/types" "8.60.1" "@typescript-eslint/types" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.1" "@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/utils" "8.60.1" "@typescript-eslint/utils" "8.60.0"
debug "^4.4.3" debug "^4.4.3"
ts-api-utils "^2.5.0" ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.60.1": "@typescript-eslint/types@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.0.tgz#e77ad768e933263b1960b2fe79de75fe1cc6e7db"
integrity sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==
"@typescript-eslint/types@^8.60.0":
version "8.60.1" version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4"
integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w== integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==
"@typescript-eslint/types@^8.60.1": "@typescript-eslint/typescript-estree@8.60.0":
version "8.61.0" version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz#c102196a44414481190041c99eea1d854e66001b"
integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg== integrity sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==
"@typescript-eslint/typescript-estree@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz#016630b119228bf483ddc652703a6a038f3fdd74"
integrity sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==
dependencies: dependencies:
"@typescript-eslint/project-service" "8.60.1" "@typescript-eslint/project-service" "8.60.0"
"@typescript-eslint/tsconfig-utils" "8.60.1" "@typescript-eslint/tsconfig-utils" "8.60.0"
"@typescript-eslint/types" "8.60.1" "@typescript-eslint/types" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.1" "@typescript-eslint/visitor-keys" "8.60.0"
debug "^4.4.3" debug "^4.4.3"
minimatch "^10.2.2" minimatch "^10.2.2"
semver "^7.7.3" semver "^7.7.3"
tinyglobby "^0.2.15" tinyglobby "^0.2.15"
ts-api-utils "^2.5.0" ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.60.1": "@typescript-eslint/utils@8.60.0":
version "8.60.1" version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.1.tgz#31cf566095602d9fe8ad91837d2eb520b8de762b" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.0.tgz#6110cddaef87606ae4ca6f8bf81bb5949fc8e098"
integrity sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg== integrity sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==
dependencies: dependencies:
"@eslint-community/eslint-utils" "^4.9.1" "@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.60.1" "@typescript-eslint/scope-manager" "8.60.0"
"@typescript-eslint/types" "8.60.1" "@typescript-eslint/types" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.1" "@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/visitor-keys@8.60.1": "@typescript-eslint/visitor-keys@8.60.0":
version "8.60.1" version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz#165d1d8901137b944efaf18f00ab5ecb57f06995" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz#f2c41eedd3d7b03b808369fb2e3fb40a93783ec2"
integrity sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag== integrity sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==
dependencies: dependencies:
"@typescript-eslint/types" "8.60.1" "@typescript-eslint/types" "8.60.0"
eslint-visitor-keys "^5.0.0" eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0": "@ungap/structured-clone@^1.0.0":
@@ -5578,10 +5578,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.34, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19: baseline-browser-mapping@^2.10.32, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.34" version "2.10.32"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz#dedb606362446777cfe328d30d4ee15056d06303" resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz#b6b553a4285fdd606327a617de36a5351e3aaa64"
integrity sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw== integrity sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==
batch@0.6.1: batch@0.6.1:
version "0.6.1" version "0.6.1"
@@ -5824,10 +5824,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2" lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001797: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001793:
version "1.0.30001797" version "1.0.30001793"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz#1332709e1439f01ff92085dd17001e0a45897ec0" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz#238887ddf5fcfc8c36d872394d0a78a517312a72"
integrity sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w== integrity sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==
ccount@^2.0.0: ccount@^2.0.0:
version "2.0.1" version "2.0.1"
@@ -9300,9 +9300,9 @@ jiti@^1.20.0:
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
joi@^17.9.2: joi@^17.9.2:
version "17.13.4" version "17.13.3"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.4.tgz#ad6153d97ce558eb3a3b593e0d43eab51df1c474" resolved "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz"
integrity sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ== integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==
dependencies: dependencies:
"@hapi/hoek" "^9.3.0" "@hapi/hoek" "^9.3.0"
"@hapi/topo" "^5.1.0" "@hapi/topo" "^5.1.0"
@@ -9341,7 +9341,7 @@ js-yaml@4.1.0:
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"
js-yaml@=4.1.1: js-yaml@=4.1.1, js-yaml@^4.1.0, js-yaml@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
@@ -9356,13 +9356,6 @@ js-yaml@^3.13.1:
argparse "^1.0.7" argparse "^1.0.7"
esprima "^4.0.0" esprima "^4.0.0"
js-yaml@^4.1.0, js-yaml@^4.1.1, js-yaml@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.2.0.tgz#2bd9e85682dd91bd469afb809d816043b3d49524"
integrity sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==
dependencies:
argparse "^2.0.1"
jsdoc-type-pratt-parser@^4.0.0: jsdoc-type-pratt-parser@^4.0.0:
version "4.8.0" version "4.8.0"
resolved "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.8.0.tgz" resolved "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.8.0.tgz"
@@ -13490,9 +13483,9 @@ shebang-regex@^3.0.0:
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.8.3: shell-quote@^1.8.3:
version "1.8.4" version "1.8.3"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190" resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz"
integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ== integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
shelljs@0.8.5: shelljs@0.8.5:
version "0.8.5" version "0.8.5"
@@ -14389,15 +14382,15 @@ types-ramda@^0.30.1:
dependencies: dependencies:
ts-toolbelt "^9.6.0" ts-toolbelt "^9.6.0"
typescript-eslint@^8.60.1: typescript-eslint@^8.60.0:
version "8.60.1" version "8.60.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.1.tgz#13db05c6eabb89669deec44545b788a0e9aee640" resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.0.tgz#6686fecb1f4f367c0bf0075828e93b7ecacbc62b"
integrity sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA== integrity sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==
dependencies: dependencies:
"@typescript-eslint/eslint-plugin" "8.60.1" "@typescript-eslint/eslint-plugin" "8.60.0"
"@typescript-eslint/parser" "8.60.1" "@typescript-eslint/parser" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.1" "@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/utils" "8.60.1" "@typescript-eslint/utils" "8.60.0"
typescript@~6.0.3: typescript@~6.0.3:
version "6.0.3" version "6.0.3"

View File

@@ -15,7 +15,7 @@
# limitations under the License. # limitations under the License.
# #
apiVersion: v2 apiVersion: v2
appVersion: "6.1.0" appVersion: "5.0.0"
description: Apache Superset is a modern, enterprise-ready business intelligence web application description: Apache Superset is a modern, enterprise-ready business intelligence web application
name: superset name: superset
icon: https://artifacthub.io/image/68c1d717-0e97-491f-b046-754e46f46922@2x icon: https://artifacthub.io/image/68c1d717-0e97-491f-b046-754e46f46922@2x
@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda - name: craig-rueda
email: craig@craigrueda.com email: craig@craigrueda.com
url: https://github.com/craig-rueda url: https://github.com/craig-rueda
version: 0.16.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details. version: 0.15.5 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies: dependencies:
- name: postgresql - name: postgresql
version: 16.7.27 version: 16.7.27

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset # superset
![Version: 0.16.0](https://img.shields.io/badge/Version-0.16.0-informational?style=flat-square) ![Version: 0.15.5](https://img.shields.io/badge/Version-0.15.5-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application Apache Superset is a modern, enterprise-ready business intelligence web application

View File

@@ -55,7 +55,7 @@ dependencies = [
"flask-login>=0.6.0, < 1.0", "flask-login>=0.6.0, < 1.0",
"flask-migrate>=3.1.0, <5.0", "flask-migrate>=3.1.0, <5.0",
"flask-session>=0.4.0, <1.0", "flask-session>=0.4.0, <1.0",
"flask-wtf>=1.3.0, <2.0", "flask-wtf>=1.1.0, <2.0",
"geopy", "geopy",
"greenlet>=3.0.3, <=3.5.0", "greenlet>=3.0.3, <=3.5.0",
"gunicorn>=25.3.0, <26; sys_platform != 'win32'", "gunicorn>=25.3.0, <26; sys_platform != 'win32'",
@@ -64,7 +64,7 @@ dependencies = [
"holidays>=0.45, <1", "holidays>=0.45, <1",
"humanize", "humanize",
"isodate", "isodate",
"jsonpath-ng>=1.8.0, <2", "jsonpath-ng>=1.6.1, <2",
"Mako>=1.2.2", "Mako>=1.2.2",
"markdown>=3.10.2", "markdown>=3.10.2",
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162 # marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
@@ -80,7 +80,7 @@ dependencies = [
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended "bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
# -------------------------- # --------------------------
"parsedatetime", "parsedatetime",
"paramiko>=3.4.0, <4.0", # 4.0 removed DSSKey, still referenced by sshtunnel "paramiko>=3.4.0",
"pgsanity", "pgsanity",
"Pillow>=11.0.0, <13", "Pillow>=11.0.0, <13",
"polyline>=2.0.0, <3.0", "polyline>=2.0.0, <3.0",
@@ -89,12 +89,12 @@ dependencies = [
"python-dateutil", "python-dateutil",
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies "python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
"pygeohash", "pygeohash",
"pyarrow>=24.0.0, <25", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693 "pyarrow>=16.1.0, <21", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
"pyyaml>=6.0.0, <7.0.0", "pyyaml>=6.0.0, <7.0.0",
"PyJWT>=2.4.0, <3.0", "PyJWT>=2.4.0, <3.0",
"redis>=5.0.0, <6.0", "redis>=5.0.0, <6.0",
"rison>=2.0.0, <3.0", "rison>=2.0.0, <3.0",
"selenium>=4.44.0, <5.0", "selenium>=4.14.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.4, <2.0", "shillelagh[gsheetsapi]>=1.4.4, <2.0",
"sshtunnel>=0.4.0, <0.5", "sshtunnel>=0.4.0, <0.5",
"simplejson>=3.15.0", "simplejson>=3.15.0",
@@ -107,9 +107,9 @@ dependencies = [
"typing-extensions>=4, <5", "typing-extensions>=4, <5",
"waitress; sys_platform == 'win32'", "waitress; sys_platform == 'win32'",
"watchdog>=6.0.0", "watchdog>=6.0.0",
"wtforms>=3.2.2, <4", "wtforms>=2.3.3, <4",
"wtforms-json", "wtforms-json",
"xlsxwriter>=3.2.9, <3.3", "xlsxwriter>=3.0.7, <3.3",
] ]
[project.optional-dependencies] [project.optional-dependencies]
@@ -118,10 +118,10 @@ athena = ["pyathena[pandas]>=2, <4"]
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"] aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [ bigquery = [
"pandas-gbq>=0.19.1", "pandas-gbq>=0.19.1",
"sqlalchemy-bigquery>=1.17.0", "sqlalchemy-bigquery>=1.15.0",
"google-cloud-bigquery>=3.10.0", "google-cloud-bigquery>=3.10.0",
] ]
clickhouse = ["clickhouse-connect>=1.1.1, <2.0"] clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"] cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
crate = ["sqlalchemy-cratedb>=0.41.0, <1"] crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
d1 = [ d1 = [
@@ -143,14 +143,14 @@ duckdb = ["duckdb>=1.5.2,<2", "duckdb-engine>=0.17.0"]
dynamodb = ["pydynamodb>=0.4.2"] dynamodb = ["pydynamodb>=0.4.2"]
solr = ["sqlalchemy-solr >= 0.2.0"] solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"] elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
exasol = ["sqlalchemy-exasol>=2.4.0, <8.0"] exasol = ["sqlalchemy-exasol >= 2.4.0, < 8.0"]
excel = ["xlrd>=1.2.0, <1.3"] excel = ["xlrd>=1.2.0, <1.3"]
fastmcp = [ fastmcp = [
"fastmcp>=3.2.4,<4.0", "fastmcp>=3.2.4,<4.0",
# tiktoken backs the response-size-guard token estimator. Without # tiktoken backs the response-size-guard token estimator. Without
# it, the middleware falls back to a coarser character-based # it, the middleware falls back to a coarser character-based
# heuristic that under-counts JSON-heavy MCP responses. # heuristic that under-counts JSON-heavy MCP responses.
"tiktoken>=0.13.0,<1.0", "tiktoken>=0.7.0,<1.0",
] ]
firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"] firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"] firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
@@ -161,13 +161,13 @@ hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'", "pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7.0", "pyhive[hive_pure_sasl]>=0.7.0",
"tableschema", "tableschema",
"thrift>=0.23.0, <1.0.0", "thrift>=0.14.1, <1.0.0",
"thrift_sasl>=0.4.3, < 1.0.0", "thrift_sasl>=0.4.3, < 1.0.0",
] ]
impala = ["impyla>0.16.2, <0.23"] impala = ["impyla>0.16.2, <0.23"]
kusto = ["sqlalchemy-kusto>=3.1.2, <4"] kusto = ["sqlalchemy-kusto>=3.0.0, <4"]
kylin = ["kylinpy>=2.8.1, <2.9"] kylin = ["kylinpy>=2.8.1, <2.9"]
mssql = ["pymssql>=2.3.13, <3"] mssql = ["pymssql>=2.2.8, <3"]
# motherduck is an alias for duckdb - MotherDuck works via the duckdb driver # motherduck is an alias for duckdb - MotherDuck works via the duckdb driver
motherduck = ["apache-superset[duckdb]"] motherduck = ["apache-superset[duckdb]"]
mysql = ["mysqlclient>=2.1.0, <3"] mysql = ["mysqlclient>=2.1.0, <3"]
@@ -180,7 +180,7 @@ ocient = [
oracle = ["cx-Oracle>8.0.0, <8.4"] oracle = ["cx-Oracle>8.0.0, <8.4"]
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"] parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"] pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.60.0, <2"] playwright = ["playwright>=1.37.0, <2"]
postgres = ["psycopg2-binary==2.9.12"] postgres = ["psycopg2-binary==2.9.12"]
presto = ["pyhive[presto]>=0.6.5"] presto = ["pyhive[presto]>=0.6.5"]
trino = ["trino>=0.328.0"] trino = ["trino>=0.328.0"]
@@ -195,19 +195,19 @@ spark = [
"pyhive[hive]>=0.6.5;python_version<'3.11'", "pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7", "pyhive[hive_pure_sasl]>=0.7",
"tableschema", "tableschema",
"thrift>=0.23.0, <1", "thrift>=0.14.1, <1",
] ]
tdengine = [ tdengine = [
"taospy>=2.7.21", "taospy>=2.7.21",
"taos-ws-py>=0.6.9" "taos-ws-py>=0.3.8"
] ]
teradata = ["teradatasql>=16.20.0.23"] teradata = ["teradatasql>=16.20.0.23"]
thumbnails = [] # deprecated, will be removed in 7.0 thumbnails = [] # deprecated, will be removed in 7.0
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"] vertica = ["sqlalchemy-vertica-python>= 0.5.9, < 0.7"]
netezza = ["nzalchemy>=11.0.2"] netezza = ["nzalchemy>=11.0.2"]
starrocks = ["starrocks>=1.3.3, <2"] starrocks = ["starrocks>=1.0.0"]
doris = ["pydoris>=1.0.0, <2.0.0"] doris = ["pydoris>=1.0.0, <2.0.0"]
oceanbase = ["oceanbase_py>=0.0.1.2"] oceanbase = ["oceanbase_py>=0.0.1"]
ydb = ["ydb-sqlalchemy>=0.1.2", "ydb-sqlglot-plugin>=0.2.5"] ydb = ["ydb-sqlalchemy>=0.1.2", "ydb-sqlglot-plugin>=0.2.5"]
development = [ development = [
# no bounds for apache-superset-extensions-cli until a stable version # no bounds for apache-superset-extensions-cli until a stable version
@@ -222,7 +222,7 @@ development = [
"pip", "pip",
"polib", # used by scripts/translations/ and their unit tests "polib", # used by scripts/translations/ and their unit tests
"pre-commit", "pre-commit",
"progress>=1.6.1,<2", "progress>=1.5,<2",
"psutil", "psutil",
"pyfakefs", "pyfakefs",
"pyinstrument>=5.1.2,<6", "pyinstrument>=5.1.2,<6",
@@ -231,7 +231,7 @@ development = [
"pytest-asyncio", "pytest-asyncio",
"pytest-cov", "pytest-cov",
"pytest-mock", "pytest-mock",
"python-ldap>=3.4.7", "python-ldap>=3.4.4",
"ruff", "ruff",
"sqloxide", "sqloxide",
"statsd", "statsd",
@@ -447,7 +447,6 @@ requirement_txt_file = "requirements/base.txt"
authorized_licenses = [ authorized_licenses = [
"academic free license (afl)", "academic free license (afl)",
"any-osi", "any-osi",
"apache-2.0",
"apache license 2.0", "apache license 2.0",
"apache software", "apache software",
"apache software, bsd", "apache software, bsd",

View File

@@ -30,7 +30,7 @@ cryptography>=46.0.7,<47.0.0
# Security: Snyk - XSS vulnerability in Mako templates # Security: Snyk - XSS vulnerability in Mako templates
mako>=1.3.11,<2.0.0 mako>=1.3.11,<2.0.0
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers # Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers
pyarrow>=24.0.0,<25.0.0 pyarrow>=20.0.0,<21.0.0
# Security: CVE-2026-27459 - pyopenssl certificate validation # Security: CVE-2026-27459 - pyopenssl certificate validation
pyopenssl>=26.0.0,<27.0.0 pyopenssl>=26.0.0,<27.0.0
# Security: CVE-2026-25645 (MEDIUM) - Insecure Temporary File # Security: CVE-2026-25645 (MEDIUM) - Insecure Temporary File

View File

@@ -50,7 +50,7 @@ cattrs==25.1.1
# via requests-cache # via requests-cache
celery==5.5.2 celery==5.5.2
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
certifi==2026.5.20 certifi==2025.6.15
# via # via
# requests # requests
# selenium # selenium
@@ -151,7 +151,7 @@ flask-sqlalchemy==2.5.1
# flask-migrate # flask-migrate
flask-talisman==1.1.0 flask-talisman==1.1.0
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
flask-wtf==1.3.0 flask-wtf==1.2.2
# via # via
# apache-superset (pyproject.toml) # apache-superset (pyproject.toml)
# flask-appbuilder # flask-appbuilder
@@ -194,7 +194,7 @@ jinja2==3.1.6
# via # via
# flask # flask
# flask-babel # flask-babel
jsonpath-ng==1.8.0 jsonpath-ng==1.7.0
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
jsonschema==4.23.0 jsonschema==4.23.0
# via # via
@@ -286,13 +286,15 @@ pillow==12.2.0
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
platformdirs==4.3.8 platformdirs==4.3.8
# via requests-cache # via requests-cache
ply==3.11
# via jsonpath-ng
polyline==2.0.2 polyline==2.0.2
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
prison==0.2.1 prison==0.2.1
# via flask-appbuilder # via flask-appbuilder
prompt-toolkit==3.0.51 prompt-toolkit==3.0.51
# via click-repl # via click-repl
pyarrow==24.0.0 pyarrow==20.0.0
# via # via
# -r requirements/base.in # -r requirements/base.in
# apache-superset (pyproject.toml) # apache-superset (pyproject.toml)
@@ -378,7 +380,7 @@ rpds-py==0.25.0
# referencing # referencing
rsa==4.9.1 rsa==4.9.1
# via google-auth # via google-auth
selenium==4.44.0 selenium==4.32.0
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
setuptools==80.9.0 setuptools==80.9.0
# via -r requirements/base.in # via -r requirements/base.in
@@ -421,7 +423,7 @@ sshtunnel==0.4.0
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
tabulate==0.10.0 tabulate==0.10.0
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
trio==0.33.0 trio==0.30.0
# via # via
# selenium # selenium
# trio-websocket # trio-websocket
@@ -478,7 +480,7 @@ wrapt==1.17.2
# via deprecated # via deprecated
wsproto==1.2.0 wsproto==1.2.0
# via trio-websocket # via trio-websocket
wtforms==3.2.2 wtforms==3.2.1
# via # via
# apache-superset (pyproject.toml) # apache-superset (pyproject.toml)
# flask-appbuilder # flask-appbuilder
@@ -488,7 +490,7 @@ wtforms-json==0.3.5
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
xlrd==2.0.1 xlrd==2.0.1
# via pandas # via pandas
xlsxwriter==3.2.9 xlsxwriter==3.0.9
# via # via
# apache-superset (pyproject.toml) # apache-superset (pyproject.toml)
# pandas # pandas

View File

@@ -112,7 +112,7 @@ celery==5.5.2
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset
certifi==2026.5.20 certifi==2025.6.15
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# httpcore # httpcore
@@ -312,7 +312,7 @@ flask-talisman==1.1.0
# apache-superset # apache-superset
flask-testing==0.8.1 flask-testing==0.8.1
# via apache-superset # via apache-superset
flask-wtf==1.3.0 flask-wtf==1.2.2
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset
@@ -471,7 +471,7 @@ jmespath==1.1.0
# via # via
# boto3 # boto3
# botocore # botocore
jsonpath-ng==1.8.0 jsonpath-ng==1.7.0
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset
@@ -674,6 +674,10 @@ platformdirs==4.3.8
# virtualenv # virtualenv
pluggy==1.5.0 pluggy==1.5.0
# via pytest # via pytest
ply==3.11
# via
# -c requirements/base-constraint.txt
# jsonpath-ng
polib==1.2.0 polib==1.2.0
# via apache-superset # via apache-superset
polyline==2.0.2 polyline==2.0.2
@@ -686,7 +690,7 @@ prison==0.2.1
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# flask-appbuilder # flask-appbuilder
progress==1.6.1 progress==1.6
# via apache-superset # via apache-superset
prompt-toolkit==3.0.51 prompt-toolkit==3.0.51
# via # via
@@ -711,7 +715,7 @@ psycopg2-binary==2.9.12
# via apache-superset # via apache-superset
py-key-value-aio==0.4.4 py-key-value-aio==0.4.4
# via fastmcp # via fastmcp
pyarrow==24.0.0 pyarrow==20.0.0
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset
@@ -834,7 +838,7 @@ python-dotenv==1.2.2
# apache-superset # apache-superset
# fastmcp # fastmcp
# pydantic-settings # pydantic-settings
python-ldap==3.4.7 python-ldap==3.4.5
# via apache-superset # via apache-superset
python-multipart==0.0.29 python-multipart==0.0.29
# via mcp # via mcp
@@ -921,7 +925,7 @@ s3transfer==0.16.0
# via boto3 # via boto3
secretstorage==3.5.0 secretstorage==3.5.0
# via keyring # via keyring
selenium==4.44.0 selenium==4.32.0
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset
@@ -976,7 +980,7 @@ sqlalchemy==1.4.54
# shillelagh # shillelagh
# sqlalchemy-bigquery # sqlalchemy-bigquery
# sqlalchemy-utils # sqlalchemy-utils
sqlalchemy-bigquery==1.17.0 sqlalchemy-bigquery==1.15.0
# via apache-superset # via apache-superset
sqlalchemy-utils==0.42.0 sqlalchemy-utils==0.42.0
# via # via
@@ -1007,7 +1011,7 @@ tabulate==0.10.0
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset
tiktoken==0.13.0 tiktoken==0.12.0
# via apache-superset # via apache-superset
tomli-w==1.2.0 tomli-w==1.2.0
# via apache-superset-extensions-cli # via apache-superset-extensions-cli
@@ -1019,7 +1023,7 @@ tqdm==4.67.1
# prophet # prophet
trino==0.330.0 trino==0.330.0
# via apache-superset # via apache-superset
trio==0.33.0 trio==0.30.0
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# selenium # selenium
@@ -1121,7 +1125,7 @@ wsproto==1.2.0
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# trio-websocket # trio-websocket
wtforms==3.2.2 wtforms==3.2.1
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset
@@ -1136,7 +1140,7 @@ xlrd==2.0.1
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# pandas # pandas
xlsxwriter==3.2.9 xlsxwriter==3.0.9
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset

View File

@@ -55,21 +55,10 @@ msgcat --sort-by-msgid --no-wrap --no-location superset/translations/messages.po
cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \ cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \
&& mv messages.pot.tmp superset/translations/messages.pot && mv messages.pot.tmp superset/translations/messages.pot
# --no-fuzzy-matching: when a *new* source string is added, Babel's fuzzy
# matcher otherwise guesses a "close" existing translation and marks it
# `#, fuzzy` in every language catalog. Those guesses are (a) usually wrong
# (e.g. a new "valuename" string mapped onto an unrelated "table name"
# translation) and (b) counted by check_translation_regression.py as a
# regression, so every PR that merely adds a translatable string failed the
# babel-extract check. Disabling fuzzy matching means new strings land as
# cleanly untranslated (empty msgstr) instead — accurate, and no spurious
# regression. Renames likewise drop the stale translation rather than
# stranding a wrong guess; the string is re-translated by the community.
pybabel update \ pybabel update \
-i superset/translations/messages.pot \ -i superset/translations/messages.pot \
-d superset/translations \ -d superset/translations \
--ignore-obsolete \ --ignore-obsolete
--no-fuzzy-matching
# Chop off last blankline from po/pot files, see https://github.com/python-babel/babel/issues/799 # Chop off last blankline from po/pot files, see https://github.com/python-babel/babel/issues/799
for file in $( find superset/translations/** ); for file in $( find superset/translations/** );

View File

@@ -20,21 +20,20 @@ Check that source-code changes don't cause translation regressions.
What counts as a regression What counts as a regression
--------------------------- ---------------------------
A regression is an *existing translation that a source change invalidated*. A regression is an *existing translation that a source change invalidated*
The check keys on the **increase in fuzzy entries** rather than a drop in the i.e. a string was renamed/reworded so its committed translation no longer
translated count, because a count drop happens identically for a benign applies. ``babel_update.sh`` (``pybabel update --ignore-obsolete``) surfaces
*deletion* and a real *rename*, so it cannot distinguish the two — whereas a exactly these as **newly fuzzy** entries: the old translation is fuzzy-matched
``#, fuzzy`` marker unambiguously flags a stranded translation. onto the new ``msgid`` and flagged ``#, fuzzy``.
Note ``babel_update.sh`` runs ``pybabel update`` with ``--no-fuzzy-matching``, Crucially, *deleting* a translatable string is **not** a regression. With
so *adding* (or renaming) a source string does **not** auto-generate a fuzzy ``--ignore-obsolete`` a removed string is dropped from the catalogs entirely;
guess against an unrelated existing translation — new strings land as cleanly no fuzzy entry is created. So a PR that intentionally removes a string (e.g. a
untranslated (empty ``msgstr``). This deliberately avoids the prior behaviour security fix that stops rendering a value) legitimately lowers the translated
where *every* PR that merely added a translatable string tripped this check on count without introducing any fuzzies, and must not be flagged. We therefore
spurious fuzzies. As a result the check now guards against ``#, fuzzy`` entries key the check on the **increase in fuzzy entries**, not on a drop in the
that arrive another way — e.g. a committed ``.po`` edit — rather than ones the translated count (a drop happens identically for a benign deletion and a real
update step synthesises. *Deleting* a string is still not a regression: with rename, so it cannot distinguish the two).
``--ignore-obsolete`` it is simply dropped and no fuzzy is created.
Usage Usage
----- -----

View File

@@ -1 +1 @@
v24.16.0 v22.22.0

View File

@@ -29,8 +29,8 @@ Embedding is done by inserting an iframe, containing a Superset page, into the h
## Prerequisites ## Prerequisites
- Activate the feature flag `EMBEDDED_SUPERSET` * Activate the feature flag `EMBEDDED_SUPERSET`
- Set a strong password in configuration variable `GUEST_TOKEN_JWT_SECRET` (see configuration file config.py). Be aware that its default value must be changed in production. * Set a strong password in configuration variable `GUEST_TOKEN_JWT_SECRET` (see configuration file config.py). Be aware that its default value must be changed in production.
## Embedding a Dashboard ## Embedding a Dashboard
@@ -41,37 +41,32 @@ npm install --save @superset-ui/embedded-sdk
``` ```
```js ```js
import { embedDashboard } from '@superset-ui/embedded-sdk'; import { embedDashboard } from "@superset-ui/embedded-sdk";
embedDashboard({ embedDashboard({
id: 'abc123', // given by the Superset embedding UI id: "abc123", // given by the Superset embedding UI
supersetDomain: 'https://superset.example.com', supersetDomain: "https://superset.example.com",
mountPoint: document.getElementById('my-superset-container'), // any html element that can contain an iframe mountPoint: document.getElementById("my-superset-container"), // any html element that can contain an iframe
fetchGuestToken: () => fetchGuestTokenFromBackend(), fetchGuestToken: () => fetchGuestTokenFromBackend(),
dashboardUiConfig: { dashboardUiConfig: { // dashboard UI config: hideTitle, hideTab, hideChartControls, filters.visible, filters.expanded (optional), urlParams (optional)
// dashboard UI config: hideTitle, hideTab, hideChartControls, filters.visible, filters.expanded (optional), urlParams (optional) hideTitle: true,
hideTitle: true, filters: {
filters: { expanded: true,
expanded: true, },
}, urlParams: {
urlParams: { foo: 'value1',
foo: 'value1', bar: 'value2',
bar: 'value2', // ...
// themeMode: 'dark', // set the initial theme: 'dark' | 'system' | 'default' (default: 'default') }
// ...
},
}, },
// optional additional iframe sandbox attributes // optional additional iframe sandbox attributes
iframeSandboxExtras: [ iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox'],
'allow-top-navigation',
'allow-popups-to-escape-sandbox',
],
// optional Permissions Policy features // optional Permissions Policy features
iframeAllowExtras: ['clipboard-write', 'fullscreen'], iframeAllowExtras: ['clipboard-write', 'fullscreen'],
// optional config to enforce a particular referrerPolicy // optional config to enforce a particular referrerPolicy
referrerPolicy: 'same-origin', referrerPolicy: "same-origin",
// optional callback to customize permalink URLs // optional callback to customize permalink URLs
resolvePermalinkUrl: ({ key }) => `https://my-app.com/analytics/share/${key}`, resolvePermalinkUrl: ({ key }) => `https://my-app.com/analytics/share/${key}`
}); });
``` ```
@@ -102,7 +97,7 @@ Guest tokens can have Row Level Security rules which filter data for the user ca
The agent making the `POST` request must be authenticated with the `can_grant_guest_token` permission. The agent making the `POST` request must be authenticated with the `can_grant_guest_token` permission.
Within your app, using the Guest Token will then allow authentication to your Superset instance via creating an Anonymous user object. This guest anonymous user will default to the public role as per this setting `GUEST_ROLE_NAME = "Public"`. Within your app, using the Guest Token will then allow authentication to your Superset instance via creating an Anonymous user object. This guest anonymous user will default to the public role as per this setting `GUEST_ROLE_NAME = "Public"`.
The user parameters in the example below are optional and are provided as a means of passing user attributes that may be accessed in jinja templates inside your charts. The user parameters in the example below are optional and are provided as a means of passing user attributes that may be accessed in jinja templates inside your charts.
@@ -115,13 +110,13 @@ Example `POST /security/guest_token` payload:
"first_name": "Stan", "first_name": "Stan",
"last_name": "Lee" "last_name": "Lee"
}, },
"resources": [ "resources": [{
{ "type": "dashboard",
"type": "dashboard", "id": "abc123"
"id": "abc123" }],
} "rls": [
], { "clause": "publisher = 'Nintendo'" }
"rls": [{ "clause": "publisher = 'Nintendo'" }] ]
} }
``` ```
@@ -157,43 +152,15 @@ In this example, the configuration file includes the following setting:
GUEST_TOKEN_JWT_AUDIENCE="superset" GUEST_TOKEN_JWT_AUDIENCE="superset"
``` ```
### Setting the Initial Theme Mode
Use the `themeMode` URL parameter to control the embedded dashboard's initial colour scheme:
```js
embedDashboard({
id: 'abc123',
supersetDomain: 'https://superset.example.com',
mountPoint: document.getElementById('my-superset-container'),
fetchGuestToken: () => fetchGuestTokenFromBackend(),
dashboardUiConfig: {
urlParams: {
themeMode: 'dark', // 'dark' | 'system' | 'default' (default: 'default')
},
},
});
```
The supported values are:
| Value | Behaviour |
| --------- | --------------------------------------------------------- |
| `default` | Light theme (Superset default) |
| `dark` | Dark theme |
| `system` | Follows the user's OS preference (`prefers-color-scheme`) |
The theme can also be changed at runtime via `embeddedDashboard.setThemeMode(mode)`.
### Sandbox iframe ### Sandbox iframe
The Embedded SDK creates an iframe with [sandbox](https://developer.mozilla.org/es/docs/Web/HTML/Element/iframe#sandbox) mode by default The Embedded SDK creates an iframe with [sandbox](https://developer.mozilla.org/es/docs/Web/HTML/Element/iframe#sandbox) mode by default
which applies certain restrictions to the iframe's content. which applies certain restrictions to the iframe's content.
To pass additional sandbox attributes you can use `iframeSandboxExtras`: To pass additional sandbox attributes you can use `iframeSandboxExtras`:
```js ```js
// optional additional iframe sandbox attributes // optional additional iframe sandbox attributes
iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox']; iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox']
``` ```
### Permissions Policy ### Permissions Policy
@@ -201,12 +168,11 @@ iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox'];
To enable specific browser features within the embedded iframe, use `iframeAllowExtras` to set the iframe's [Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy) (the `allow` attribute): To enable specific browser features within the embedded iframe, use `iframeAllowExtras` to set the iframe's [Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy) (the `allow` attribute):
```js ```js
// optional Permissions Policy features // optional Permissions Policy features
iframeAllowExtras: ['clipboard-write', 'fullscreen']; iframeAllowExtras: ['clipboard-write', 'fullscreen']
``` ```
Common permissions you might need: Common permissions you might need:
- `clipboard-write` - Required for "Copy permalink to clipboard" functionality - `clipboard-write` - Required for "Copy permalink to clipboard" functionality
- `fullscreen` - Required for fullscreen chart viewing - `fullscreen` - Required for fullscreen chart viewing
- `camera`, `microphone` - If your dashboards include media capture features - `camera`, `microphone` - If your dashboards include media capture features
@@ -225,16 +191,16 @@ When users click share buttons inside an embedded dashboard, Superset generates
```js ```js
embedDashboard({ embedDashboard({
id: 'abc123', id: "abc123",
supersetDomain: 'https://superset.example.com', supersetDomain: "https://superset.example.com",
mountPoint: document.getElementById('my-superset-container'), mountPoint: document.getElementById("my-superset-container"),
fetchGuestToken: () => fetchGuestTokenFromBackend(), fetchGuestToken: () => fetchGuestTokenFromBackend(),
// Customize permalink URLs // Customize permalink URLs
resolvePermalinkUrl: ({ key }) => { resolvePermalinkUrl: ({ key }) => {
// key: the permalink key (e.g., "xyz789") // key: the permalink key (e.g., "xyz789")
return `https://my-app.com/analytics/share/${key}`; return `https://my-app.com/analytics/share/${key}`;
}, }
}); });
``` ```
@@ -245,15 +211,15 @@ To restore the dashboard state from a permalink in your app:
const permalinkKey = routeParams.key; const permalinkKey = routeParams.key;
embedDashboard({ embedDashboard({
id: 'abc123', id: "abc123",
supersetDomain: 'https://superset.example.com', supersetDomain: "https://superset.example.com",
mountPoint: document.getElementById('my-superset-container'), mountPoint: document.getElementById("my-superset-container"),
fetchGuestToken: () => fetchGuestTokenFromBackend(), fetchGuestToken: () => fetchGuestTokenFromBackend(),
resolvePermalinkUrl: ({ key }) => `https://my-app.com/analytics/share/${key}`, resolvePermalinkUrl: ({ key }) => `https://my-app.com/analytics/share/${key}`,
dashboardUiConfig: { dashboardUiConfig: {
urlParams: { urlParams: {
permalink_key: permalinkKey, // Restores filters, tabs, chart states, and scrolls to anchor permalink_key: permalinkKey, // Restores filters, tabs, chart states, and scrolls to anchor
}, }
}, }
}); });
``` ```

View File

@@ -22,7 +22,6 @@ import {
getGuestTokenRefreshTiming, getGuestTokenRefreshTiming,
MIN_REFRESH_WAIT_MS, MIN_REFRESH_WAIT_MS,
DEFAULT_TOKEN_EXP_MS, DEFAULT_TOKEN_EXP_MS,
DEFAULT_TOKEN_REFRESH_RETRY_MS,
} from "./guestTokenRefresh"; } from "./guestTokenRefresh";
describe("guest token refresh", () => { describe("guest token refresh", () => {
@@ -94,11 +93,4 @@ describe("guest token refresh", () => {
expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_MS); expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_MS);
expect(timing).toBe(DEFAULT_TOKEN_EXP_MS - REFRESH_TIMING_BUFFER_MS); expect(timing).toBe(DEFAULT_TOKEN_EXP_MS - REFRESH_TIMING_BUFFER_MS);
}); });
it("exposes a positive retry delay for failed token refreshes", () => {
// The refresh loop reschedules itself after this delay when a fetch
// fails or times out, so it must be a sane positive value.
expect(DEFAULT_TOKEN_REFRESH_RETRY_MS).toBe(10000);
expect(DEFAULT_TOKEN_REFRESH_RETRY_MS).toBeGreaterThan(0);
});
}); });

View File

@@ -21,7 +21,6 @@ import { jwtDecode } from "jwt-decode";
export const REFRESH_TIMING_BUFFER_MS = 5000 // refresh guest token early to avoid failed superset requests export const REFRESH_TIMING_BUFFER_MS = 5000 // refresh guest token early to avoid failed superset requests
export const MIN_REFRESH_WAIT_MS = 10000 // avoid blasting requests as fast as the cpu can handle export const MIN_REFRESH_WAIT_MS = 10000 // avoid blasting requests as fast as the cpu can handle
export const DEFAULT_TOKEN_EXP_MS = 300000 // (5 min) used only when parsing guest token exp fails export const DEFAULT_TOKEN_EXP_MS = 300000 // (5 min) used only when parsing guest token exp fails
export const DEFAULT_TOKEN_REFRESH_RETRY_MS = 10000 // wait before retrying a failed/timed-out token refresh
// when do we refresh the guest token? // when do we refresh the guest token?
export function getGuestTokenRefreshTiming(currentGuestToken: string) { export function getGuestTokenRefreshTiming(currentGuestToken: string) {

View File

@@ -24,11 +24,7 @@ import {
// We can swap this out for the actual switchboard package once it gets published // We can swap this out for the actual switchboard package once it gets published
import { Switchboard } from '@superset-ui/switchboard'; import { Switchboard } from '@superset-ui/switchboard';
import { import { getGuestTokenRefreshTiming } from './guestTokenRefresh';
getGuestTokenRefreshTiming,
DEFAULT_TOKEN_REFRESH_RETRY_MS,
} from './guestTokenRefresh';
import { withTimeout } from './withTimeout';
/** /**
* The function to fetch a guest token from your Host App's backend server. * The function to fetch a guest token from your Host App's backend server.
@@ -53,9 +49,6 @@ export type UiConfigType = {
showRowLimitWarning?: boolean; showRowLimitWarning?: boolean;
}; };
/** Default per-call timeout (ms) applied to the host `fetchGuestToken` callback. */
const DEFAULT_GUEST_TOKEN_FETCH_TIMEOUT_MS = 30_000;
export type EmbedDashboardParams = { export type EmbedDashboardParams = {
/** The id provided by the embed configuration UI in Superset */ /** The id provided by the embed configuration UI in Superset */
id: string; id: string;
@@ -80,10 +73,6 @@ export type EmbedDashboardParams = {
/** Callback to resolve permalink URLs. If provided, this will be called when generating permalinks /** Callback to resolve permalink URLs. If provided, this will be called when generating permalinks
* to allow the host app to customize the URL. If not provided, Superset's default URL is used. */ * to allow the host app to customize the URL. If not provided, Superset's default URL is used. */
resolvePermalinkUrl?: ResolvePermalinkUrlFn; resolvePermalinkUrl?: ResolvePermalinkUrlFn;
/** Timeout, in milliseconds, applied to each `fetchGuestToken` call so a host
* callback that never resolves cannot hang the embed/refresh cycle. Defaults
* to 30000ms. Set to 0 to disable the timeout. */
guestTokenFetchTimeoutMs?: number;
}; };
export type Size = { export type Size = {
@@ -138,7 +127,6 @@ export async function embedDashboard({
iframeAllowExtras = [], iframeAllowExtras = [],
referrerPolicy, referrerPolicy,
resolvePermalinkUrl, resolvePermalinkUrl,
guestTokenFetchTimeoutMs = DEFAULT_GUEST_TOKEN_FETCH_TIMEOUT_MS,
}: EmbedDashboardParams): Promise<EmbeddedDashboard> { }: EmbedDashboardParams): Promise<EmbeddedDashboard> {
function log(...info: unknown[]) { function log(...info: unknown[]) {
if (debug) { if (debug) {
@@ -146,16 +134,6 @@ export async function embedDashboard({
} }
} }
// Wrap the host-provided fetchGuestToken so a callback that never settles
// cannot hang the initial embed or a later refresh cycle.
function fetchGuestTokenWithTimeout(): Promise<string> {
return withTimeout(
fetchGuestToken(),
guestTokenFetchTimeoutMs,
'fetchGuestToken',
);
}
log('embedding'); log('embedding');
if (supersetDomain.endsWith('/')) { if (supersetDomain.endsWith('/')) {
@@ -269,57 +247,21 @@ export async function embedDashboard({
}); });
} }
let guestToken: string; const [guestToken, ourPort]: [string, Switchboard] = await Promise.all([
let ourPort: Switchboard; fetchGuestToken(),
try { mountIframe(),
[guestToken, ourPort] = await Promise.all([ ]);
fetchGuestTokenWithTimeout(),
mountIframe(),
]);
} catch (err) {
// If the initial token fetch (or timeout) rejects after the iframe has
// already been mounted, tear down the partially initialized iframe so the
// host isn't left with an orphaned embedded dashboard before rethrowing.
//@ts-ignore
mountPoint.replaceChildren();
throw err;
}
ourPort.emit('guestToken', { guestToken }); ourPort.emit('guestToken', { guestToken });
log('sent guest token'); log('sent guest token');
// Track the pending refresh timer so it can be cancelled on unmount, and
// stop the cycle once unmounted so it cannot leak across mount/unmount cycles.
let refreshTimer: ReturnType<typeof setTimeout> | undefined;
let unmounted = false;
async function refreshGuestToken() { async function refreshGuestToken() {
if (unmounted) return; const newGuestToken = await fetchGuestToken();
try { ourPort.emit('guestToken', { guestToken: newGuestToken });
const newGuestToken = await fetchGuestTokenWithTimeout(); setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(newGuestToken));
if (unmounted) return;
ourPort.emit('guestToken', { guestToken: newGuestToken });
refreshTimer = setTimeout(
refreshGuestToken,
getGuestTokenRefreshTiming(newGuestToken),
);
} catch (err) {
// A transient fetch failure or timeout must not permanently stop the
// refresh cycle. Log it and retry so the session can recover once the
// host callback succeeds again.
log('failed to refresh guest token, will retry:', err);
if (unmounted) return;
refreshTimer = setTimeout(
refreshGuestToken,
DEFAULT_TOKEN_REFRESH_RETRY_MS,
);
}
} }
refreshTimer = setTimeout( setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(guestToken));
refreshGuestToken,
getGuestTokenRefreshTiming(guestToken),
);
// Register the resolvePermalinkUrl method for the iframe to call // Register the resolvePermalinkUrl method for the iframe to call
// Returns null if no callback provided or on error, allowing iframe to use default URL // Returns null if no callback provided or on error, allowing iframe to use default URL
@@ -341,11 +283,6 @@ export async function embedDashboard({
function unmount() { function unmount() {
log('unmounting'); log('unmounting');
unmounted = true;
if (refreshTimer !== undefined) {
clearTimeout(refreshTimer);
refreshTimer = undefined;
}
//@ts-ignore //@ts-ignore
mountPoint.replaceChildren(); mountPoint.replaceChildren();
} }

View File

@@ -1,39 +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 { withTimeout } from "./withTimeout";
test("resolves with the value when the promise settles in time", async () => {
await expect(withTimeout(Promise.resolve("ok"), 1000, "fetch")).resolves.toBe(
"ok"
);
});
test("rejects when the promise does not settle within the timeout", async () => {
const never = new Promise<string>(() => {});
await expect(withTimeout(never, 10, "fetch")).rejects.toThrow(
/fetch did not resolve within 10ms/
);
});
test("passes the promise through unchanged when the timeout is disabled", async () => {
await expect(withTimeout(Promise.resolve("ok"), 0, "fetch")).resolves.toBe(
"ok"
);
});

View File

@@ -1,43 +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.
*/
/**
* Rejects if `promise` does not settle within `ms` milliseconds. A non-positive
* `ms` disables the timeout and returns the promise unchanged. The timer is
* always cleared so it cannot keep the event loop alive.
*/
export function withTimeout<T>(
promise: Promise<T>,
ms: number,
label: string,
): Promise<T> {
if (!ms || ms <= 0) {
return promise;
}
let timer: ReturnType<typeof setTimeout>;
const timeout = new Promise<never>((_resolve, reject) => {
timer = setTimeout(
() => reject(new Error(`${label} did not resolve within ${ms}ms`)),
ms,
);
});
return Promise.race([promise, timeout]).finally(() =>
clearTimeout(timer),
) as Promise<T>;
}

View File

@@ -226,7 +226,7 @@ def copy_frontend_dist(cwd: Path) -> str:
def copy_backend_files(cwd: Path) -> None: def copy_backend_files(cwd: Path) -> None:
"""Copy backend files based on pyproject.toml build configuration (validation already passed).""" """Copy backend files based on pyproject.toml build configuration (validation already passed)."""
dist_dir = cwd / "dist" dist_dir = cwd / "dist"
backend_dir = (cwd / "backend").resolve() backend_dir = cwd / "backend"
# Read build config from pyproject.toml # Read build config from pyproject.toml
pyproject = read_toml(backend_dir / "pyproject.toml") pyproject = read_toml(backend_dir / "pyproject.toml")
@@ -239,31 +239,11 @@ def copy_backend_files(cwd: Path) -> None:
# Process include patterns # Process include patterns
for pattern in include_patterns: for pattern in include_patterns:
# Include patterns are only meant to select files within the backend
# directory. Reject absolute patterns or ones that walk outside it via
# parent ("..") components before handing them to glob().
pattern_parts = Path(pattern).parts
if Path(pattern).is_absolute() or ".." in pattern_parts:
raise click.ClickException(
f"Invalid include pattern {pattern!r}: patterns must be "
"relative to the backend directory and may not contain '..'."
)
for f in backend_dir.glob(pattern): for f in backend_dir.glob(pattern):
if not f.is_file(): if not f.is_file():
continue continue
# Defense in depth: confirm the matched file resolves to a location # Check exclude patterns
# inside the backend directory before copying it into the bundle.
resolved = f.resolve()
if not resolved.is_relative_to(backend_dir):
raise click.ClickException(
f"Refusing to copy {f}: resolved path is outside the "
f"backend directory {backend_dir}."
)
# Use the matched path (not the resolved target) for the bundle
# layout and exclude evaluation so symlinked files are staged at
# their configured path rather than their symlink target.
relative_path = f.relative_to(backend_dir) relative_path = f.relative_to(backend_dir)
should_exclude = any( should_exclude = any(
relative_path.match(excl_pattern) for excl_pattern in exclude_patterns relative_path.match(excl_pattern) for excl_pattern in exclude_patterns

View File

@@ -20,7 +20,6 @@ from __future__ import annotations
import json import json
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import click
import pytest import pytest
from superset_extensions_cli.cli import ( from superset_extensions_cli.cli import (
app, app,
@@ -626,155 +625,6 @@ exclude = []
) )
@pytest.mark.unit
def test_copy_backend_files_supports_legitimate_nested_patterns(isolated_filesystem):
"""Test copy_backend_files copies deeply nested files via recursive globs."""
backend_dir = isolated_filesystem / "backend"
nested = backend_dir / "src" / "test_org" / "test_ext" / "deep" / "deeper"
nested.mkdir(parents=True)
(nested / "module.py").write_text("# nested module")
pyproject_content = """[project]
name = "test_org-test_ext"
version = "1.0.0"
license = "Apache-2.0"
[tool.apache_superset_extensions.build]
include = [
"src/test_org/test_ext/**/*.py",
]
exclude = []
"""
(backend_dir / "pyproject.toml").write_text(pyproject_content)
extension_data = {
"publisher": "test-org",
"name": "test-ext",
"displayName": "Test Extension",
"version": "1.0.0",
"permissions": [],
}
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_data))
clean_dist(isolated_filesystem)
copy_backend_files(isolated_filesystem)
dist_dir = isolated_filesystem / "dist"
assert_file_exists(
dist_dir
/ "backend"
/ "src"
/ "test_org"
/ "test_ext"
/ "deep"
/ "deeper"
/ "module.py"
)
@pytest.mark.unit
@pytest.mark.parametrize(
"bad_pattern",
[
"../../.ssh/*",
"../config",
"src/../../secret.txt",
"/etc/passwd",
],
)
def test_copy_backend_files_rejects_patterns_escaping_backend_dir(
isolated_filesystem, bad_pattern
):
"""Test copy_backend_files refuses include patterns that escape backend_dir."""
# Create a sensitive file outside the backend directory.
(isolated_filesystem / "secret.txt").write_text("SECRET")
(isolated_filesystem / "config").write_text("SECRET")
backend_dir = isolated_filesystem / "backend"
backend_src = backend_dir / "src" / "test_org" / "test_ext"
backend_src.mkdir(parents=True)
(backend_src / "__init__.py").write_text("# init")
pyproject_content = f"""[project]
name = "test_org-test_ext"
version = "1.0.0"
license = "Apache-2.0"
[tool.apache_superset_extensions.build]
include = [
"{bad_pattern}",
]
exclude = []
"""
(backend_dir / "pyproject.toml").write_text(pyproject_content)
extension_data = {
"publisher": "test-org",
"name": "test-ext",
"displayName": "Test Extension",
"version": "1.0.0",
"permissions": [],
}
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_data))
clean_dist(isolated_filesystem)
with pytest.raises(click.ClickException):
copy_backend_files(isolated_filesystem)
# Nothing outside the backend directory should have been staged into dist,
# including paths reachable via ".." from inside dist/backend.
dist_dir = isolated_filesystem / "dist"
assert not (dist_dir / "secret.txt").exists()
assert not (dist_dir / "config").exists()
@pytest.mark.unit
def test_copy_backend_files_stages_symlink_at_matched_path(isolated_filesystem):
"""Symlinked files inside backend are staged at the matched path, not the target."""
backend_dir = isolated_filesystem / "backend"
target_dir = backend_dir / "src" / "common"
target_dir.mkdir(parents=True)
(target_dir / "module.py").write_text("# shared module")
link_dir = backend_dir / "src" / "test_org" / "test_ext" / "common"
link_dir.mkdir(parents=True)
link = link_dir / "module.py"
link.symlink_to(target_dir / "module.py")
pyproject_content = """[project]
name = "test_org-test_ext"
version = "1.0.0"
license = "Apache-2.0"
[tool.apache_superset_extensions.build]
include = [
"src/test_org/test_ext/**/*.py",
]
exclude = []
"""
(backend_dir / "pyproject.toml").write_text(pyproject_content)
extension_data = {
"publisher": "test-org",
"name": "test-ext",
"displayName": "Test Extension",
"version": "1.0.0",
"permissions": [],
}
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_data))
clean_dist(isolated_filesystem)
copy_backend_files(isolated_filesystem)
dist_dir = isolated_filesystem / "dist"
# Staged at the configured (symlink) path, not the resolved target path.
assert_file_exists(
dist_dir / "backend" / "src" / "test_org" / "test_ext" / "common" / "module.py"
)
assert not (dist_dir / "backend" / "src" / "common" / "module.py").exists()
# Removed obsolete tests: # Removed obsolete tests:
# - test_copy_backend_files_handles_no_backend_config: This scenario can't happen since copy_backend_files is only called when backend exists # - test_copy_backend_files_handles_no_backend_config: This scenario can't happen since copy_backend_files is only called when backend exists
# - test_copy_backend_files_exits_when_extension_json_missing: Validation catches this before copy_backend_files is called # - test_copy_backend_files_exits_when_extension_json_missing: Validation catches this before copy_backend_files is called

View File

@@ -0,0 +1,34 @@
#
# 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.
#
**/*{.,-}min.js
**/*.sh
coverage/**
dist/*
src/assets/images/*
node_modules/*
node_modules*/*
vendor/*
docs/*
src/dashboard/deprecated/*
src/temp/*
**/node_modules
*.d.ts
coverage/
esm/
lib/
tmp/
storybook-static/

View File

@@ -0,0 +1,523 @@
/**
* 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.
*/
// Register TypeScript require hook so ESLint can load .ts plugin files
require('tsx/cjs');
const packageConfig = require('./package.json');
const importCoreModules = [];
Object.entries(packageConfig.dependencies).forEach(([pkg]) => {
if (/@superset-ui/.test(pkg)) {
importCoreModules.push(pkg);
}
});
// ignore files in production mode
let ignorePatterns = [];
if (process.env.NODE_ENV === 'production') {
ignorePatterns = [
'*.test.{js,ts,jsx,tsx}',
'plugins/**/test/**/*',
'packages/**/test/**/*',
'packages/generator-superset/**/*',
];
}
const restrictedImportsRules = {
'no-design-icons': {
name: '@ant-design/icons',
message:
'Avoid importing icons directly from @ant-design/icons. Use the src/components/Icons component instead.',
},
'no-moment': {
name: 'moment',
message:
'Please use the dayjs library instead of moment.js. See https://day.js.org',
},
'no-lodash-memoize': {
name: 'lodash/memoize',
message: 'Lodash Memoize is unsafe! Please use memoize-one instead',
},
'no-testing-library-react': {
name: '@superset-ui/core/spec',
message: 'Please use spec/helpers/testing-library instead',
},
'no-testing-library-react-dom-utils': {
name: '@testing-library/react-dom-utils',
message: 'Please use spec/helpers/testing-library instead',
},
'no-antd': {
name: 'antd',
message: 'Please import Ant components from the index of src/components',
},
'no-superset-theme': {
name: '@superset-ui/core',
importNames: ['supersetTheme'],
message:
'Please use the theme directly from the ThemeProvider rather than importing supersetTheme.',
},
'no-query-string': {
name: 'query-string',
message: 'Please use the URLSearchParams API instead of query-string.',
},
'no-jest-mock-console': {
name: 'jest-mock-console',
message: 'Please use native Jest spies, i.e. jest.spyOn(console, "warn")',
},
};
module.exports = {
extends: [
'eslint:recommended',
'plugin:import/recommended',
'plugin:react-prefer-function-component/recommended',
'plugin:storybook/recommended',
'prettier',
],
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
requireConfigFile: false,
babelOptions: {
presets: ['@babel/preset-react', '@babel/preset-env'],
},
},
env: {
browser: true,
node: true,
es2020: true,
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
moduleDirectory: ['node_modules', '.'],
},
typescript: {
alwaysTryTypes: true,
project: [
'./tsconfig.json',
'./packages/superset-ui-core/tsconfig.json',
'./packages/superset-ui-chart-controls/',
'./plugins/*/tsconfig.json',
],
},
},
'import/core-modules': importCoreModules,
react: {
version: 'detect',
},
},
plugins: [
'import',
'lodash',
'theme-colors',
'icons',
'i18n-strings',
'react-prefer-function-component',
'react-you-might-not-need-an-effect',
'prettier',
],
rules: {
// === Essential Superset customizations ===
// Prettier integration
'prettier/prettier': 'error',
// Custom Superset rules
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': 'error',
// Core ESLint overrides for Superset
'no-console': 'warn',
'no-unused-vars': 'off', // TypeScript handles this
camelcase: [
'error',
{
allow: ['^UNSAFE_', '__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'],
properties: 'never',
},
],
'prefer-destructuring': ['error', { object: true, array: false }],
'no-prototype-builtins': 0,
curly: 'off',
// Import plugin overrides
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-cycle': 0,
'import/prefer-default-export': 0,
'import/no-named-as-default-member': 0,
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'test/**',
'tests/**',
'spec/**',
'**/__tests__/**',
'**/__mocks__/**',
'*.test.{js,jsx,ts,tsx}',
'*.spec.{js,jsx,ts,tsx}',
'**/*.test.{js,jsx,ts,tsx}',
'**/*.spec.{js,jsx,ts,tsx}',
'**/jest.config.js',
'**/jest.setup.js',
'**/webpack.config.js',
'**/webpack.config.*.js',
'**/.eslintrc*.js',
],
optionalDependencies: false,
},
],
// React plugin overrides
'react-prefer-function-component/react-prefer-function-component': 1,
// React effect best practices
'react-you-might-not-need-an-effect/no-empty-effect': 'error',
'react-you-might-not-need-an-effect/no-pass-live-state-to-parent': 'error',
'react-you-might-not-need-an-effect/no-initialize-state': 'error',
// Lodash
'lodash/import-scope': [2, 'member'],
// React effect best practices
'react-you-might-not-need-an-effect/no-reset-all-state-on-prop-change':
'error',
'react-you-might-not-need-an-effect/no-chain-state-updates': 'error',
'react-you-might-not-need-an-effect/no-event-handler': 'error',
'react-you-might-not-need-an-effect/no-derived-state': 'error',
// Storybook
'storybook/prefer-pascal-case': 'error',
// File progress
'file-progress/activate': 1,
// React effect rules
'react-you-might-not-need-an-effect/no-adjust-state-on-prop-change':
'error',
'react-you-might-not-need-an-effect/no-pass-data-to-parent': 'error',
// Restricted imports
'no-restricted-imports': [
'error',
{
paths: Object.values(restrictedImportsRules).filter(Boolean),
patterns: ['antd/*'],
},
],
// Temporarily disabled for migration
'no-unsafe-optional-chaining': 0,
'no-import-assign': 0,
'import/no-relative-packages': 0,
'no-promise-executor-return': 0,
'import/no-import-module-exports': 0,
// Restrict certain syntax patterns
'no-restricted-syntax': [
'error',
{
selector:
"ImportDeclaration[source.value='react'] :matches(ImportDefaultSpecifier, ImportNamespaceSpecifier)",
message:
'Default React import is not required due to automatic JSX runtime in React 16.4',
},
{
selector: 'ImportNamespaceSpecifier[parent.source.value!=/^(\\.|src)/]',
message: 'Wildcard imports are not allowed',
},
],
},
overrides: [
// Ban JavaScript files in src/ - all new code must be TypeScript
{
files: ['src/**/*.js', 'src/**/*.jsx'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message:
'JavaScript files are not allowed in src/. Please use TypeScript (.ts/.tsx) instead.',
},
],
},
},
// Ban JavaScript files in plugins/ - all plugin source code must be TypeScript
{
files: ['plugins/**/src/**/*.js', 'plugins/**/src/**/*.jsx'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message:
'JavaScript files are not allowed in plugins/. Please use TypeScript (.ts/.tsx) instead.',
},
],
},
},
// Ban JavaScript files in packages/ - with exceptions for config files and generators
{
files: ['packages/**/src/**/*.js', 'packages/**/src/**/*.jsx'],
excludedFiles: [
'packages/generator-superset/**/*', // Yeoman generator templates run via Node
'packages/**/__mocks__/**/*', // Test mocks
],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message:
'JavaScript files are not allowed in packages/. Please use TypeScript (.ts/.tsx) instead.',
},
],
},
},
{
files: ['*.ts', '*.tsx'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: ['plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['@typescript-eslint/eslint-plugin'],
rules: {
// TypeScript-specific rule overrides
'@typescript-eslint/ban-ts-ignore': 0,
'@typescript-eslint/ban-ts-comment': 0,
'@typescript-eslint/ban-types': 0,
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'enum',
format: ['PascalCase'],
},
{
selector: 'enumMember',
format: ['PascalCase'],
},
],
'@typescript-eslint/no-empty-function': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-use-before-define': 'error',
'@typescript-eslint/no-non-null-assertion': 0,
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/prefer-optional-chain': 'error',
// Disable base rules that conflict with TS versions
'no-unused-vars': 'off',
'no-use-before-define': 'off',
'no-shadow': 'off',
// Import overrides for TypeScript
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
},
settings: {
'import/resolver': {
typescript: {},
},
},
},
{
files: ['packages/**'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
'no-restricted-imports': [
'error',
{
paths: [
restrictedImportsRules['no-moment'],
restrictedImportsRules['no-lodash-memoize'],
restrictedImportsRules['no-superset-theme'],
],
patterns: [],
},
],
},
},
{
files: ['plugins/**'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
restrictedImportsRules['no-moment'],
restrictedImportsRules['no-lodash-memoize'],
],
patterns: [],
},
],
},
},
{
files: ['src/components/**', 'src/theme/**'],
rules: {
'no-restricted-imports': [
'error',
{
paths: Object.values(restrictedImportsRules).filter(
r => r.name !== 'antd',
),
patterns: [],
},
],
},
},
{
files: [
'*.test.ts',
'*.test.tsx',
'*.test.js',
'*.test.jsx',
'*.stories.tsx',
'*.stories.jsx',
'fixtures.*',
'**/test/**/*',
'**/tests/**/*',
'spec/**/*',
'**/fixtures/**/*',
'**/__mocks__/**/*',
'**/spec/**/*',
],
excludedFiles: 'cypress-base/cypress/**/*',
plugins: ['jest-dom', 'no-only-tests', 'testing-library'],
extends: ['plugin:jest-dom/recommended', 'plugin:testing-library/react'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
'prefer-promise-reject-errors': 0,
'max-classes-per-file': 0,
// Temporary for migration
'testing-library/await-async-queries': 0,
'testing-library/await-async-utils': 0,
'testing-library/no-await-sync-events': 0,
'testing-library/no-render-in-lifecycle': 0,
'testing-library/no-unnecessary-act': 0,
'testing-library/no-wait-for-multiple-assertions': 0,
'testing-library/prefer-screen-queries': 0,
'testing-library/await-async-events': 0,
'testing-library/no-node-access': 0,
'testing-library/no-wait-for-side-effects': 0,
'testing-library/prefer-presence-queries': 0,
'testing-library/render-result-naming-convention': 0,
'testing-library/no-container': 0,
'testing-library/prefer-find-by': 0,
'testing-library/no-manual-cleanup': 0,
'no-restricted-syntax': [
'error',
{
selector:
"ImportDeclaration[source.value='react'] :matches(ImportDefaultSpecifier, ImportNamespaceSpecifier)",
message:
'Default React import is not required due to automatic JSX runtime in React 16.4',
},
],
'no-restricted-imports': 0,
},
},
{
files: [
'*.test.ts',
'*.test.tsx',
'*.test.js',
'*.test.jsx',
'*.stories.tsx',
'*.stories.jsx',
'fixtures.*',
'**/test/**/*',
'**/tests/**/*',
'spec/**/*',
'**/fixtures/**/*',
'**/__mocks__/**/*',
'**/spec/**/*',
'cypress-base/cypress/**/*',
'Stories.tsx',
'packages/superset-ui-core/src/theme/index.tsx',
],
rules: {
'theme-colors/no-literal-colors': 0,
'icons/no-fa-icons-usage': 0,
'i18n-strings/no-template-vars': 0,
'no-restricted-imports': 0,
},
},
{
files: [
'packages/**/*.stories.*',
'packages/**/*.overview.*',
'packages/**/fixtures.*',
],
rules: {
'import/no-extraneous-dependencies': 'off',
},
},
{
files: ['playwright/**/*.ts', 'playwright/**/*.js'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
},
},
],
ignorePatterns,
};

View File

@@ -0,0 +1,124 @@
/**
* 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.
*/
// Register TypeScript require hook so ESLint can load .ts plugin files
require('tsx/cjs');
/**
* MINIMAL ESLint config - ONLY for rules OXC doesn't support
* This config is designed to be run alongside OXC linter
*
* Only covers:
* - Custom Superset plugins (theme-colors, icons, i18n)
* - Prettier formatting
* - File progress indicator
*/
module.exports = {
root: true,
// Don't report on eslint-disable comments for rules we don't have
reportUnusedDisableDirectives: false,
// Simple parser - no TypeScript needed since OXC handles that
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
requireConfigFile: false,
babelOptions: {
presets: ['@babel/preset-react', '@babel/preset-env'],
},
},
env: {
browser: true,
node: true,
es2020: true,
},
plugins: [
// ONLY custom Superset plugins that OXC doesn't support
'theme-colors',
'icons',
'i18n-strings',
'file-progress',
'prettier',
],
rules: {
// === ONLY rules that OXC cannot handle ===
// Prettier integration (formatting)
'prettier/prettier': 'error',
// Custom Superset plugins
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': 'error',
'file-progress/activate': 1,
// Explicitly turn off all other rules to avoid conflicts
// when the config gets merged with other configs
'import/no-unresolved': 'off',
'import/extensions': 'off',
'@typescript-eslint/naming-convention': 'off',
},
overrides: [
{
// Disable custom rules in test/story files
files: [
'**/*.test.*',
'**/*.spec.*',
'**/*.stories.*',
'**/test/**',
'**/tests/**',
'**/spec/**',
'**/__tests__/**',
'**/__mocks__/**',
'cypress-base/**',
'packages/superset-ui-core/src/theme/index.tsx',
],
rules: {
'theme-colors/no-literal-colors': 0,
'icons/no-fa-icons-usage': 0,
'i18n-strings/no-template-vars': 0,
'file-progress/activate': 0,
},
},
],
// Only check src/ files where theme/icon rules matter
ignorePatterns: [
'node_modules',
'dist',
'build',
'.next',
'coverage',
'*.min.js',
'vendor',
// Skip packages/plugins since they have different theming rules
'packages/**',
'plugins/**',
// Skip generated/external files
'*.generated.*',
'*.config.js',
'webpack.*',
// Temporary analysis files
'*.js', // Skip all standalone JS files in root
'*.json',
],
};

View File

@@ -1 +1 @@
v24.16.0 v22.22.0

View File

@@ -1,3 +1,4 @@
import { dirname, join } from 'path';
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
@@ -16,16 +17,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
// This file has been automatically migrated to valid ESM format by Storybook.
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
// Superset's webpack.config.js // Superset's webpack.config.js
import customConfig from '../webpack.config.js'; const customConfig = require('../webpack.config.js');
const require = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Filter out plugins that shouldn't be included in Storybook's static build // Filter out plugins that shouldn't be included in Storybook's static build
// ReactRefreshWebpackPlugin adds Fast Refresh code that requires a dev server runtime, // ReactRefreshWebpackPlugin adds Fast Refresh code that requires a dev server runtime,
@@ -83,7 +76,7 @@ const disableDevModeInRules = rules =>
}; };
}); });
export default { module.exports = {
stories: [ stories: [
'../src/**/*.stories.tsx', '../src/**/*.stories.tsx',
'../packages/superset-ui-core/src/**/*.stories.tsx', '../packages/superset-ui-core/src/**/*.stories.tsx',
@@ -91,8 +84,11 @@ export default {
], ],
addons: [ addons: [
"@storybook/addon-links", getAbsolutePath('@storybook/addon-essentials'),
"@storybook/addon-docs" getAbsolutePath('@storybook/addon-links'),
'@mihkeleidast/storybook-addon-source',
getAbsolutePath('@storybook/addon-controls'),
getAbsolutePath('@storybook/addon-mdx-gfm'),
], ],
staticDirs: ['../src/assets/images'], staticDirs: ['../src/assets/images'],
@@ -109,13 +105,11 @@ export default {
alias: { alias: {
...config.resolve?.alias, ...config.resolve?.alias,
...customConfig.resolve?.alias, ...customConfig.resolve?.alias,
// Fix for Storybook 8.6.x with React 17 - resolve ESM module paths
'react-dom/test-utils': require.resolve('react-dom/test-utils'),
// Shared storybook utilities // Shared storybook utilities
'@storybook-shared': path.join(__dirname, 'shared'), '@storybook-shared': join(__dirname, 'shared'),
}, },
fallback: {
tty: false,
vm: require.resolve('vm-browserify')
}
}, },
plugins: [...config.plugins, ...filteredPlugins], plugins: [...config.plugins, ...filteredPlugins],
}), }),
@@ -125,11 +119,15 @@ export default {
}, },
framework: { framework: {
name: getAbsolutePath("@storybook/react-webpack5"), name: getAbsolutePath('@storybook/react-webpack5'),
options: {}, options: {},
} },
docs: {
autodocs: false,
},
}; };
function getAbsolutePath(value) { function getAbsolutePath(value) {
return path.dirname(require.resolve(path.join(value, 'package.json'))); return dirname(require.resolve(join(value, 'package.json')));
} }

View File

@@ -16,6 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { withJsx } from '@mihkeleidast/storybook-addon-source';
import { themeObject, css, exampleThemes } from '@apache-superset/core/theme'; import { themeObject, css, exampleThemes } from '@apache-superset/core/theme';
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'; import { combineReducers, createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
@@ -113,12 +114,9 @@ const providerDecorator = Story => (
</Provider> </Provider>
); );
export const decorators = [themeDecorator, providerDecorator]; export const decorators = [withJsx, themeDecorator, providerDecorator];
export const parameters = { export const parameters = {
docs: {
codePanel: true,
},
paddings: { paddings: {
values: [ values: [
{ name: 'None', value: '0px' }, { name: 'None', value: '0px' },

View File

@@ -19,7 +19,7 @@
import { useState, ReactNode, SyntheticEvent } from 'react'; import { useState, ReactNode, SyntheticEvent } from 'react';
import { styled } from '@apache-superset/core/theme'; import { styled } from '@apache-superset/core/theme';
import type { Decorator } from '@storybook/react-webpack5'; import type { Decorator } from '@storybook/react';
import { ResizeCallbackData } from 'react-resizable'; import { ResizeCallbackData } from 'react-resizable';
import ResizablePanel, { Size } from './ResizablePanel'; import ResizablePanel, { Size } from './ResizablePanel';

View File

@@ -48,7 +48,6 @@ module.exports = {
'@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-export-namespace-from', '@babel/plugin-transform-export-namespace-from',
['@babel/plugin-transform-class-properties', { loose: true }], ['@babel/plugin-transform-class-properties', { loose: true }],
'@babel/plugin-transform-class-static-block',
['@babel/plugin-transform-optional-chaining', { loose: true }], ['@babel/plugin-transform-optional-chaining', { loose: true }],
['@babel/plugin-transform-private-methods', { loose: true }], ['@babel/plugin-transform-private-methods', { loose: true }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }], ['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],

View File

@@ -0,0 +1,67 @@
/**
* 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 { SAMPLE_DASHBOARD_1 } from 'cypress/utils/urls';
import { interceptFav, interceptUnfav } from './utils';
describe('Dashboard actions', () => {
beforeEach(() => {
cy.createSampleDashboards([0]);
cy.visit(SAMPLE_DASHBOARD_1);
});
it('should allow to favorite/unfavorite dashboard', () => {
interceptFav();
interceptUnfav();
// Find and click StarOutlined (adds to favorites)
cy.getBySel('dashboard-header-container')
.find("[aria-label='unstarred']")
.as('starIconOutlined')
.should('exist')
.click();
cy.wait('@select');
// After clicking, StarFilled should appear
cy.getBySel('dashboard-header-container')
.find("[aria-label='starred']")
.as('starIconFilled')
.should('exist');
// Verify the color of the filled star (gold)
cy.get('@starIconFilled')
.should('have.css', 'color')
.and('eq', 'rgb(252, 199, 0)');
// Click on StarFilled (removes from favorites)
cy.get('@starIconFilled').click();
cy.wait('@unselect');
// After clicking, StarOutlined should reappear
cy.getBySel('dashboard-header-container')
.find("[aria-label='unstarred']")
.as('starIconOutlinedAfter')
.should('exist');
// Verify the color of the outlined star (gray)
cy.get('@starIconOutlinedAfter')
.should('have.css', 'color')
.and('eq', 'rgba(0, 0, 0, 0.45)');
});
});

View File

@@ -160,6 +160,18 @@ export function interceptLog() {
cy.intercept('**/superset/log/?explode=events&dashboard_id=*').as('logs'); cy.intercept('**/superset/log/?explode=events&dashboard_id=*').as('logs');
} }
export function interceptFav() {
cy.intercept({ url: `**/api/v1/dashboard/*/favorites/`, method: 'POST' }).as(
'select',
);
}
export function interceptUnfav() {
cy.intercept({ url: `**/api/v1/dashboard/*/favorites/`, method: 'POST' }).as(
'unselect',
);
}
export function interceptDataset() { export function interceptDataset() {
cy.intercept('GET', `**/api/v1/dataset/*`).as('getDataset'); cy.intercept('GET', `**/api/v1/dataset/*`).as('getDataset');
} }

View File

@@ -2058,24 +2058,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@istanbuljs/schema": { "node_modules/@istanbuljs/schema": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz",
@@ -2952,8 +2934,6 @@
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"peer": true,
"dependencies": { "dependencies": {
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
@@ -4373,8 +4353,6 @@
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"peer": true,
"bin": { "bin": {
"esparse": "bin/esparse.js", "esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js" "esvalidate": "bin/esvalidate.js"
@@ -5616,9 +5594,7 @@
"version": "3.14.2", "version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"esprima": "^4.0.0" "esprima": "^4.0.0"
@@ -7780,9 +7756,7 @@
"node_modules/sprintf-js": { "node_modules/sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
"dev": true,
"peer": true
}, },
"node_modules/sshpk": { "node_modules/sshpk": {
"version": "1.18.0", "version": "1.18.0",
@@ -10228,23 +10202,8 @@
"camelcase": "^5.3.1", "camelcase": "^5.3.1",
"find-up": "^4.1.0", "find-up": "^4.1.0",
"get-package-type": "^0.1.0", "get-package-type": "^0.1.0",
"js-yaml": "4.1.1", "js-yaml": "^3.13.1",
"resolve-from": "^5.0.0" "resolve-from": "^5.0.0"
},
"dependencies": {
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"requires": {
"argparse": "^2.0.1"
}
}
} }
}, },
"@istanbuljs/schema": { "@istanbuljs/schema": {
@@ -11047,8 +11006,6 @@
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"peer": true,
"requires": { "requires": {
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
@@ -12094,9 +12051,7 @@
"esprima": { "esprima": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
"dev": true,
"peer": true
}, },
"esquery": { "esquery": {
"version": "1.4.0", "version": "1.4.0",
@@ -12998,8 +12953,6 @@
"version": "3.14.2", "version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"peer": true,
"requires": { "requires": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"esprima": "^4.0.0" "esprima": "^4.0.0"
@@ -14510,9 +14463,7 @@
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
"dev": true,
"peer": true
}, },
"sshpk": { "sshpk": {
"version": "1.18.0", "version": "1.18.0",

View File

@@ -65,86 +65,6 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
}; };
}, },
}, },
'no-eager-t-in-config': {
meta: {
type: 'problem',
fixable: 'code',
docs: {
description:
'Disallow eager t()/tn() calls for `label` and `description` in config objects evaluated at module load (e.g., controlPanel files). The translation is captured at module-evaluation time, before i18n has loaded, and never updates when the user switches language. Wrap the call in an arrow function so it is evaluated at render time.',
},
schema: [
{
type: 'object',
properties: {
properties: {
type: 'array',
items: { type: 'string' },
},
},
additionalProperties: false,
},
],
messages: {
eager:
'Eager `{{property}}: {{fn}}(...)` is evaluated at module load, before i18n is initialized. Wrap in an arrow function: `{{property}}: () => {{fn}}(...)`.',
},
},
create(context: Rule.RuleContext): Rule.RuleListener {
const watchedProps: string[] = context.options[0]?.properties ?? [
'label',
'description',
];
const TRANSLATE_FNS = new Set(['t', 'tn']);
function handler(node: Node): void {
const prop = node as Node & {
key: { type: string; name?: string; value?: string };
value: Node & {
type: string;
callee?: { type: string; name?: string };
};
shorthand?: boolean;
computed?: boolean;
};
if (prop.shorthand || prop.computed) return;
const keyName =
prop.key.type === 'Identifier'
? prop.key.name
: prop.key.type === 'Literal'
? prop.key.value
: undefined;
if (typeof keyName !== 'string' || !watchedProps.includes(keyName)) {
return;
}
const callee = prop.value;
if (
callee.type !== 'CallExpression' ||
callee.callee?.type !== 'Identifier' ||
!callee.callee.name ||
!TRANSLATE_FNS.has(callee.callee.name)
) {
return;
}
context.report({
node: prop.value,
messageId: 'eager',
data: { property: keyName, fn: callee.callee.name },
fix(fixer) {
const source = context.getSourceCode().getText(prop.value);
return fixer.replaceText(prop.value, `() => ${source}`);
},
});
}
return {
Property: handler,
};
},
},
'sentence-case-buttons': { 'sentence-case-buttons': {
meta: { meta: {
type: 'suggestion', type: 'suggestion',

View File

@@ -1,86 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { Rule } from 'eslint';
const { RuleTester } = require('eslint');
const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
const ruleTester = new RuleTester({
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
});
const rule: Rule.RuleModule = plugin.rules['no-eager-t-in-config'];
ruleTester.run('no-eager-t-in-config', rule, {
valid: [
// Lazy form — the recommended pattern
"const c = { label: () => t('Foo') };",
"const c = { description: () => t('Foo') };",
"const c = { label: () => tn('one', 'many', n) };",
// Static strings — no translation, no issue
"const c = { label: 'Foo' };",
// Other property names — unaffected
"const c = { name: t('Foo') };",
"const c = { title: t('Foo') };",
// Computed keys are too dynamic to lint usefully
"const c = { [labelKey]: t('Foo') };",
// Shorthand: `{ label }` — no value to inspect
'const label = t("Foo"); const c = { label };',
// t() called inside a function body — already lazy
"const c = { label: state => t('Foo') };",
// Non-t() call expressions are fine
"const c = { label: someOtherFn('Foo') };",
],
invalid: [
{
code: "const c = { label: t('Foo') };",
output: "const c = { label: () => t('Foo') };",
errors: [{ messageId: 'eager' }],
},
{
code: "const c = { description: t('Foo bar') };",
output: "const c = { description: () => t('Foo bar') };",
errors: [{ messageId: 'eager' }],
},
{
code: "const c = { label: tn('one', 'many', 2) };",
output: "const c = { label: () => tn('one', 'many', 2) };",
errors: [{ messageId: 'eager' }],
},
// String-literal keys are equivalent to identifier keys
{
code: "const c = { 'label': t('Foo') };",
output: "const c = { 'label': () => t('Foo') };",
errors: [{ messageId: 'eager' }],
},
// Custom watched-property list via rule option
{
code: "const c = { headerTitle: t('Foo') };",
output: "const c = { headerTitle: () => t('Foo') };",
options: [{ properties: ['headerTitle'] }],
errors: [{ messageId: 'eager' }],
},
// Nested config — fires per occurrence
{
code: "const c = { foo: { label: t('A'), description: t('B') } };",
output:
"const c = { foo: { label: () => t('A'), description: () => t('B') } };",
errors: [{ messageId: 'eager' }, { messageId: 'eager' }],
},
],
});

View File

@@ -31,13 +31,12 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
// Tests // Tests
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 6 } }); const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
const rule: Rule.RuleModule = plugin.rules['no-template-vars']; const rule: Rule.RuleModule = plugin.rules['no-template-vars'];
const errors: Array<{ message: string }> = [ const errors: Array<{ type: string }> = [
{ {
message: type: 'CallExpression',
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it can't handle strings that include variables",
}, },
]; ];

View File

@@ -31,10 +31,7 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
// Tests // Tests
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const ruleTester = new RuleTester({ const ruleTester = new RuleTester({
languageOptions: { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } },
ecmaVersion: 6,
parserOptions: { ecmaFeatures: { jsx: true } },
},
}); });
const rule: Rule.RuleModule = plugin.rules['no-fa-icons-usage']; const rule: Rule.RuleModule = plugin.rules['no-fa-icons-usage'];

View File

@@ -1,137 +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.
*/
/**
* MINIMAL ESLint flat config - ONLY for rules OXC doesn't support.
*
* This config is run alongside the OXC (oxlint) linter, which handles the
* bulk of linting. ESLint here only covers the custom Superset plugins and
* Prettier formatting that oxlint cannot express. It is consumed by
* `scripts/oxlint-metrics-uploader.js` (`npm run lint-stats`).
*
* Migrated from the legacy `.eslintrc.minimal.js` (eslintrc) format to flat
* config for ESLint v9+/v10, where eslintrc is no longer supported.
*
* Only covers:
* - Custom Superset plugins (theme-colors, icons, i18n-strings)
* - Prettier formatting
*/
// Register the TypeScript require hook so ESLint can load the .ts plugin files
// from eslint-rules/*.
require('tsx/cjs');
const tsParser = require('@typescript-eslint/parser');
const prettierPlugin = require('eslint-plugin-prettier');
const themeColorsPlugin = require('eslint-plugin-theme-colors');
const iconsPlugin = require('eslint-plugin-icons');
const i18nStringsPlugin = require('eslint-plugin-i18n-strings');
module.exports = [
// Files this config applies to. Flat config has no `--ext`; globs live here.
// Only check src/ files where the theme/icon/i18n rules matter.
{
ignores: [
'node_modules/**',
'dist/**',
'build/**',
'.next/**',
'coverage/**',
'**/*.min.js',
'vendor/**',
// Skip packages/plugins since they have different theming rules
'packages/**',
'plugins/**',
// Skip generated/external/config files
'**/*.generated.*',
'**/*.config.js',
'**/webpack.*',
'*.json',
],
},
{
files: ['**/*.{js,jsx,ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module',
// The @typescript-eslint parser handles both TS/TSX and plain JS/JSX and
// is compatible with ESLint v10's scope manager. (The legacy
// @babel/eslint-parser does not support ESLint v10.) The custom rules
// here are pure AST visitors and do not require type information, so no
// `project` is configured — this keeps parsing fast.
parser: tsParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
// Don't report on eslint-disable comments for rules we don't have.
linterOptions: {
reportUnusedDisableDirectives: false,
},
plugins: {
prettier: prettierPlugin,
'theme-colors': themeColorsPlugin,
icons: iconsPlugin,
'i18n-strings': i18nStringsPlugin,
},
rules: {
// Prettier integration (formatting)
'prettier/prettier': 'error',
// Custom Superset plugins
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': 'error',
// Enabled only for controlPanel files via the override below.
'i18n-strings/no-eager-t-in-config': 'off',
},
},
{
// Eager t()/tn() in `label`/`description` config props is captured at
// module-load time, before i18n initializes — labels stay in the fallback
// language even after the user switches. Surfaced as a warning (with
// autofix to `() => t(...)`) wherever this is a real foot-gun:
// controlPanel files. Promote to `'error'` once the codebase is clean.
files: ['**/controlPanel.{ts,tsx,js,jsx}'],
rules: {
'i18n-strings/no-eager-t-in-config': 'warn',
},
},
{
// Disable custom rules in test/story files
files: [
'**/*.test.*',
'**/*.spec.*',
'**/*.stories.*',
'**/test/**',
'**/tests/**',
'**/spec/**',
'**/__tests__/**',
'**/__mocks__/**',
'cypress-base/**',
],
rules: {
'theme-colors/no-literal-colors': 'off',
'icons/no-fa-icons-usage': 'off',
'i18n-strings/no-template-vars': 'off',
},
},
];

View File

@@ -69,7 +69,7 @@ module.exports = {
], ],
coverageReporters: ['lcov', 'json-summary', 'html', 'text'], coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
transformIgnorePatterns: [ transformIgnorePatterns: [
'node_modules/(?!@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.)', 'node_modules/(?!@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued)',
], ],
preset: 'ts-jest', preset: 'ts-jest',
transform: { transform: {

View File

@@ -287,15 +287,13 @@
"ignorePatterns": [ "ignorePatterns": [
"packages/generator-superset/**/*", "packages/generator-superset/**/*",
"cypress-base/**", "cypress-base/**",
"**/node_modules/**", "node_modules/**",
"build/**", "build/**",
"**/dist/**", "dist/**",
"**/lib/**", "lib/**",
"**/esm/**", "esm/**",
"**/*.min.js", "*.min.js",
"**/*.d.ts",
"coverage/**", "coverage/**",
"storybook-static/**",
".git/**", ".git/**",
"**/*.config.js", "**/*.config.js",
"**/*.config.ts" "**/*.config.ts"

File diff suppressed because it is too large Load Diff

View File

@@ -82,7 +82,7 @@
"prune": "rm -rf ./{packages,plugins}/*/{node_modules,lib,esm,tsconfig.tsbuildinfo,package-lock.json} ./.temp_cache", "prune": "rm -rf ./{packages,plugins}/*/{node_modules,lib,esm,tsconfig.tsbuildinfo,package-lock.json} ./.temp_cache",
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006", "storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006",
"test-storybook": "test-storybook", "test-storybook": "test-storybook",
"test-storybook:ci": "concurrently --kill-others --success first --names \"SB,TEST\" --prefix-colors \"magenta,blue\" \"npx http-server storybook-static --port 6006 --silent\" \"npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook -- --maxWorkers=2\"", "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"npx http-server storybook-static --port 6006 --silent\" \"npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook -- --maxWorkers=2\"",
"tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --watch", "tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --watch",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent", "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent",
"test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%", "test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%",
@@ -164,8 +164,8 @@
"@visx/scale": "^3.5.0", "@visx/scale": "^3.5.0",
"@visx/tooltip": "^3.0.0", "@visx/tooltip": "^3.0.0",
"@visx/xychart": "^3.5.1", "@visx/xychart": "^3.5.1",
"ag-grid-community": "35.3.1", "ag-grid-community": "35.3.0",
"ag-grid-react": "35.3.1", "ag-grid-react": "35.3.0",
"antd": "^5.26.0", "antd": "^5.26.0",
"chrono-node": "^2.9.1", "chrono-node": "^2.9.1",
"classnames": "^2.2.5", "classnames": "^2.2.5",
@@ -178,7 +178,7 @@
"echarts": "^5.6.0", "echarts": "^5.6.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fs-extra": "^11.3.5", "fs-extra": "^11.3.5",
"fuse.js": "^7.4.1", "fuse.js": "^7.3.0",
"geolib": "^3.3.14", "geolib": "^3.3.14",
"geostyler": "^18.6.0", "geostyler": "^18.6.0",
"geostyler-data": "^1.1.0", "geostyler-data": "^1.1.0",
@@ -204,7 +204,7 @@
"query-string": "9.4.0", "query-string": "9.4.0",
"re-resizable": "^6.11.2", "re-resizable": "^6.11.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-arborist": "^3.10.1", "react-arborist": "^3.8.0",
"react-checkbox-tree": "^1.8.0", "react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2", "react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3", "react-dnd": "^11.1.3",
@@ -261,17 +261,25 @@
"@babel/types": "^7.29.7", "@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2", "@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.13", "@formatjs/intl-durationformat": "^0.10.3",
"@istanbuljs/nyc-config-typescript": "^1.0.1", "@istanbuljs/nyc-config-typescript": "^1.0.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1",
"@playwright/test": "^1.60.0", "@playwright/test": "^1.60.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2", "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.2", "@storybook/addon-actions": "^8.6.18",
"@storybook/addon-links": "10.4.2", "@storybook/addon-controls": "^8.6.18",
"@storybook/react-webpack5": "10.4.2", "@storybook/addon-essentials": "^8.6.18",
"@storybook/test-runner": "0.24.4", "@storybook/addon-links": "^8.6.18",
"@storybook/addon-mdx-gfm": "^8.6.18",
"@storybook/components": "^8.6.18",
"@storybook/preview-api": "^8.6.18",
"@storybook/react": "^8.6.18",
"@storybook/react-webpack5": "^8.6.18",
"@storybook/test": "^8.6.18",
"@storybook/test-runner": "^0.17.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.40", "@swc/core": "^1.15.40",
"@swc/plugin-emotion": "^14.12.0", "@swc/plugin-emotion": "^14.10.0",
"@swc/plugin-transform-imports": "^12.5.0", "@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4", "@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
@@ -280,15 +288,16 @@
"@types/content-disposition": "^0.5.9", "@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7", "@types/dom-to-image": "^2.6.7",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/jquery": "^4.0.1", "@types/jquery": "^4.0.0",
"@types/js-levenshtein": "^1.1.3", "@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4", "@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15", "@types/mousetrap": "^1.6.15",
"@types/node": "^25.9.2", "@types/node": "^25.9.1",
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0", "@types/react-dom": "^18.2.0",
"@types/react-loadable": "^5.5.11", "@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10", "@types/react-redux": "^7.1.10",
"@types/react-resizable": "^4.0.0",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.12", "@types/react-transition-group": "^4.4.12",
"@types/react-window": "^1.8.8", "@types/react-window": "^1.8.8",
@@ -297,35 +306,35 @@
"@types/rison": "0.1.0", "@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3", "@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11", "@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.61.0", "@typescript-eslint/eslint-plugin": "^8.60.0",
"@typescript-eslint/parser": "^8.61.0", "@typescript-eslint/parser": "^8.59.4",
"babel-jest": "^30.4.1", "babel-jest": "^30.4.1",
"babel-loader": "^10.1.1", "babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0", "babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4", "babel-plugin-lodash": "^3.3.4",
"baseline-browser-mapping": "^2.10.34", "baseline-browser-mapping": "^2.10.32",
"cheerio": "1.2.0", "cheerio": "1.2.0",
"concurrently": "^10.0.3", "concurrently": "^9.2.1",
"copy-webpack-plugin": "^14.0.0", "copy-webpack-plugin": "^14.0.0",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"css-loader": "^7.1.4", "css-loader": "^7.1.4",
"css-minimizer-webpack-plugin": "^8.0.0", "css-minimizer-webpack-plugin": "^8.0.0",
"eslint": "^10.4.1", "eslint": "^8.56.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^7.2.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^4.4.5", "eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-cypress": "^3.6.0", "eslint-plugin-cypress": "^3.6.0",
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings", "eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons", "eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest-dom": "^5.5.0", "eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-lodash": "^8.0.0", "eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-no-only-tests": "^3.4.0", "eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-prettier": "^5.5.6", "eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react-prefer-function-component": "^5.0.0", "eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.0", "eslint-plugin-react-you-might-not-need-an-effect": "^0.10.4",
"eslint-plugin-storybook": "10.4.2", "eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^7.16.2", "eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors", "eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0", "fetch-mock": "^12.6.0",
@@ -344,7 +353,7 @@
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2", "mini-css-extract-plugin": "^2.10.2",
"open-cli": "^9.0.0", "open-cli": "^9.0.0",
"oxlint": "^1.68.0", "oxlint": "^1.67.0",
"po2json": "^0.4.5", "po2json": "^0.4.5",
"prettier": "3.8.3", "prettier": "3.8.3",
"prettier-plugin-packagejson": "^3.0.2", "prettier-plugin-packagejson": "^3.0.2",
@@ -355,22 +364,22 @@
"source-map": "^0.7.6", "source-map": "^0.7.6",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0", "speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.4.2", "storybook": "8.6.18",
"style-loader": "^4.0.0", "style-loader": "^4.0.0",
"swc-loader": "^0.2.7", "swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.1", "terser-webpack-plugin": "^5.6.1",
"ts-jest": "^29.4.11", "ts-jest": "^29.4.11",
"tscw-config": "^1.1.2", "tscw-config": "^1.1.2",
"tsx": "^4.22.4", "tsx": "^4.22.3",
"typescript": "5.4.5", "typescript": "5.4.5",
"unzipper": "^0.12.3", "unzipper": "^0.12.3",
"vm-browserify": "^1.1.2", "vm-browserify": "^1.1.2",
"wait-on": "^9.0.10", "wait-on": "^9.0.10",
"webpack": "^5.107.2", "webpack": "^5.107.2",
"webpack-bundle-analyzer": "^5.3.0", "webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^7.0.3", "webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.4", "webpack-dev-server": "^5.2.4",
"webpack-manifest-plugin": "^6.0.1", "webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.5.0", "webpack-sources": "^3.5.0",
"webpack-visualizer-plugin2": "^2.0.0" "webpack-visualizer-plugin2": "^2.0.0"
}, },
@@ -382,8 +391,8 @@
"regenerator-runtime": "^0.14.1" "regenerator-runtime": "^0.14.1"
}, },
"engines": { "engines": {
"node": "^24.16.0", "node": "^22.22.0",
"npm": "^11.13.0" "npm": "^10.8.1"
}, },
"overrides": { "overrides": {
"uuid": "$uuid", "uuid": "$uuid",
@@ -414,16 +423,7 @@
"@jest/types": "^30.4.0", "@jest/types": "^30.4.0",
"jest-util": "^30.4.0", "jest-util": "^30.4.0",
"jest-circus": "^30.4.0", "jest-circus": "^30.4.0",
"jest-environment-node": "^30.4.0", "jest-environment-node": "^30.4.0"
"@babel/eslint-parser": {
"eslint": "$eslint"
},
"eslint-plugin-import": {
"eslint": "$eslint"
},
"eslint-plugin-jest-dom": {
"eslint": "$eslint"
}
}, },
"readme": "ERROR: No README data found!", "readme": "ERROR: No README data found!",
"scarfSettings": { "scarfSettings": {

View File

@@ -37,7 +37,7 @@
* ``` * ```
*/ */
import { Disposable, Event } from '../common'; import { Disposable } from '../common';
/** /**
* Represents a menu item that links a view to a command. * Represents a menu item that links a view to a command.
@@ -102,37 +102,3 @@ export declare function registerMenuItem(
* ``` * ```
*/ */
export declare function getMenu(location: string): Menu | undefined; export declare function getMenu(location: string): Menu | undefined;
/**
* Event fired when a menu item is registered.
*/
export interface MenuItemRegisteredEvent {
/** The menu item that was registered. */
item: MenuItem;
/** The location where the item was registered. */
location: string;
/** The group the item was placed in. */
group: 'primary' | 'secondary' | 'context';
}
/**
* Event fired when a menu item is unregistered.
*/
export interface MenuItemUnregisteredEvent {
/** The menu item that was unregistered. */
item: MenuItem;
/** The location where the item was registered. */
location: string;
/** The group the item was placed in. */
group: 'primary' | 'secondary' | 'context';
}
/**
* Event fired when a menu item is registered.
*/
export declare const onDidRegisterMenuItem: Event<MenuItemRegisteredEvent>;
/**
* Event fired when a menu item is unregistered.
*/
export declare const onDidUnregisterMenuItem: Event<MenuItemUnregisteredEvent>;

View File

@@ -508,12 +508,6 @@ export interface ThemeContextType {
clearLocalOverrides: () => void; clearLocalOverrides: () => void;
getCurrentCrudThemeId: () => string | null; getCurrentCrudThemeId: () => string | null;
hasDevOverride: () => boolean; hasDevOverride: () => boolean;
/**
* True when an explicit theme config override is active (e.g. supplied via
* the Embedded SDK). Such an override takes precedence over a
* dashboard-level theme.
*/
hasThemeConfigOverride: boolean;
canSetMode: () => boolean; canSetMode: () => boolean;
canSetTheme: () => boolean; canSetTheme: () => boolean;
canDetectOSPreference: () => boolean; canDetectOSPreference: () => boolean;

View File

@@ -26,7 +26,7 @@ test('t() warns and creates a default translator when called before configure',
const { t } = require('./TranslatorSingleton'); const { t } = require('./TranslatorSingleton');
const result = t('hello'); const result = t('hello');
expect(consoleSpy).toHaveBeenCalledWith( expect(consoleSpy).toHaveBeenCalledWith(
expect.stringMatching(/was called before configure\(\)/), 'You should call configure(...) before calling other methods',
); );
expect(result).toBe('hello'); expect(result).toBe('hello');
consoleSpy.mockRestore(); consoleSpy.mockRestore();
@@ -54,7 +54,7 @@ test('resetTranslation resets the configured singleton', () => {
// After reset, calling t() should warn again // After reset, calling t() should warn again
t('hello'); t('hello');
expect(consoleSpy).toHaveBeenCalledWith( expect(consoleSpy).toHaveBeenCalledWith(
expect.stringMatching(/was called before configure\(\)/), 'You should call configure(...) before calling other methods',
); );
consoleSpy.mockRestore(); consoleSpy.mockRestore();
}); });
@@ -96,69 +96,6 @@ test('tn() calls translateWithNumber on the singleton', () => {
}); });
}); });
test('pre-configure warning fires once per unique key', () => {
jest.isolateModules(() => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { t } = require('./TranslatorSingleton');
t('apple');
t('apple');
t('apple');
t('banana');
expect(consoleSpy).toHaveBeenCalledTimes(2);
expect(consoleSpy).toHaveBeenNthCalledWith(
1,
expect.stringContaining('"apple"'),
);
expect(consoleSpy).toHaveBeenNthCalledWith(
2,
expect.stringContaining('"banana"'),
);
consoleSpy.mockRestore();
});
});
test('pre-configure warning suggests the lazy-function fix', () => {
jest.isolateModules(() => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { t } = require('./TranslatorSingleton');
t('Sort ascending');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('() => t("Sort ascending")'),
);
consoleSpy.mockRestore();
});
});
test('pre-configure warning is suppressed in production', () => {
jest.isolateModules(() => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { t } = require('./TranslatorSingleton');
t('hello');
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
if (originalEnv !== undefined) {
process.env.NODE_ENV = originalEnv;
} else {
delete process.env.NODE_ENV;
}
});
});
test('resetTranslation clears the warned-keys dedupe set', () => {
jest.isolateModules(() => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { t, resetTranslation } = require('./TranslatorSingleton');
t('hello');
expect(consoleSpy).toHaveBeenCalledTimes(1);
resetTranslation();
t('hello');
expect(consoleSpy).toHaveBeenCalledTimes(2);
consoleSpy.mockRestore();
});
});
test('resetTranslation does nothing when not yet configured', () => { test('resetTranslation does nothing when not yet configured', () => {
jest.isolateModules(() => { jest.isolateModules(() => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
@@ -168,7 +105,7 @@ test('resetTranslation does nothing when not yet configured', () => {
// The singleton is still unconfigured, so t() warns // The singleton is still unconfigured, so t() warns
t('hello'); t('hello');
expect(consoleSpy).toHaveBeenCalledWith( expect(consoleSpy).toHaveBeenCalledWith(
expect.stringMatching(/was called before configure\(\)/), 'You should call configure(...) before calling other methods',
); );
consoleSpy.mockRestore(); consoleSpy.mockRestore();
}); });

View File

@@ -25,10 +25,6 @@ import { TranslatorConfig, Translations, LocaleData } from './types';
let singleton: Translator | undefined; let singleton: Translator | undefined;
let isConfigured = false; let isConfigured = false;
// Tracks which keys have already triggered a pre-configure warning so the
// logs don't drown in repeated calls from large module-load fan-outs.
const warnedPreConfigureKeys = new Set<string>();
function configure(config?: TranslatorConfig) { function configure(config?: TranslatorConfig) {
singleton = new Translator(config); singleton = new Translator(config);
isConfigured = true; isConfigured = true;
@@ -37,6 +33,10 @@ function configure(config?: TranslatorConfig) {
} }
function getInstance() { function getInstance() {
if (!isConfigured) {
console.warn('You should call configure(...) before calling other methods');
}
if (typeof singleton === 'undefined') { if (typeof singleton === 'undefined') {
singleton = new Translator(); singleton = new Translator();
} }
@@ -44,32 +44,11 @@ function getInstance() {
return singleton; return singleton;
} }
function warnPreConfigure(fn: 't' | 'tn', key: string) {
// Only warn in non-production builds — production callers may legitimately
// tolerate the fallback, and the noise isn't useful at runtime.
if (
typeof process !== 'undefined' &&
process.env?.NODE_ENV === 'production'
) {
return;
}
if (warnedPreConfigureKeys.has(key)) return;
warnedPreConfigureKeys.add(key);
console.warn(
`[i18n] ${fn}(${JSON.stringify(key)}) was called before configure() — ` +
`the result is the fallback language and will not update when the ` +
`user switches language. If this call is at module load (e.g., a ` +
`controlPanel \`label\`/\`description\`), wrap it in an arrow ` +
`function: \`() => ${fn}(${JSON.stringify(key)})\`.`,
);
}
function resetTranslation() { function resetTranslation() {
if (isConfigured) { if (isConfigured) {
isConfigured = false; isConfigured = false;
singleton = undefined; singleton = undefined;
} }
warnedPreConfigureKeys.clear();
} }
function addTranslation(key: string, translations: string[]) { function addTranslation(key: string, translations: string[]) {
@@ -85,12 +64,10 @@ function addLocaleData(data: LocaleData) {
} }
function t(input: string, ...args: unknown[]) { function t(input: string, ...args: unknown[]) {
if (!isConfigured) warnPreConfigure('t', input);
return getInstance().translate(input, ...args); return getInstance().translate(input, ...args);
} }
function tn(key: string, ...args: unknown[]) { function tn(key: string, ...args: unknown[]) {
if (!isConfigured) warnPreConfigure('tn', key);
return getInstance().translateWithNumber(key, ...args); return getInstance().translateWithNumber(key, ...args);
} }

View File

@@ -36,7 +36,7 @@
*/ */
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import { Disposable, Event } from '../common'; import { Disposable } from '../common';
/** /**
* Represents a contributed view in the application. * Represents a contributed view in the application.
@@ -88,33 +88,3 @@ export declare function registerView(
* ``` * ```
*/ */
export declare function getViews(location: string): View[] | undefined; export declare function getViews(location: string): View[] | undefined;
/**
* Event fired when a view is registered.
*/
export interface ViewRegisteredEvent {
/** The descriptor of the view that was registered. */
view: View;
/** The location where the view was registered. */
location: string;
}
/**
* Event fired when a view is unregistered.
*/
export interface ViewUnregisteredEvent {
/** The descriptor of the view that was unregistered. */
view: View;
/** The location where the view was registered. */
location: string;
}
/**
* Event fired when a view is registered.
*/
export declare const onDidRegisterView: Event<ViewRegisteredEvent>;
/**
* Event fired when a view is unregistered.
*/
export declare const onDidUnregisterView: Event<ViewUnregisteredEvent>;

View File

@@ -118,6 +118,7 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
description: t(`Select dimension and values`), description: t(`Select dimension and values`),
default: { dimension: '', values: [] }, default: { dimension: '', values: [] },
validators: [], // No validation - rely on visibility validators: [], // No validation - rely on visibility
renderTrigger: true,
tabOverride: 'matrixify', tabOverride: 'matrixify',
shouldMapStateToProps: (prevState, state) => { shouldMapStateToProps: (prevState, state) => {
// Recalculate when any relevant form_data field changes // Recalculate when any relevant form_data field changes

View File

@@ -204,14 +204,8 @@ export type TabOverride = 'data' | 'customize' | 'matrixify' | boolean;
* these configs will be passed to the UI component for control as props. * these configs will be passed to the UI component for control as props.
* *
* - type: the control type, referencing a React component of the same name * - type: the control type, referencing a React component of the same name
* - label: the label as shown in the control's header. When the value involves * - label: the label as shown in the control's header
* `t()`/`tn()`, prefer the arrow-function form (`label: () => t('Foo')`) so * - description: shown in the info tooltip of the control's header
* the lookup runs at render time rather than at module load — eager
* `label: t('Foo')` captures the fallback language before i18n initializes
* and does not update on runtime language change. The
* `i18n-strings/no-eager-t-in-config` lint rule autofixes this.
* - description: shown in the info tooltip of the control's header. Same
* lazy-form guidance as `label`.
* - default: the default value when opening a new chart, or changing visualization type * - default: the default value when opening a new chart, or changing visualization type
* - renderTrigger: a bool that defines whether the visualization should be re-rendered * - renderTrigger: a bool that defines whether the visualization should be re-rendered
* when changed. This should `true` for controls that only affect the rendering (client side) * when changed. This should `true` for controls that only affect the rendering (client side)

View File

@@ -31,8 +31,8 @@
"@types/json-bigint": "^1.0.4", "@types/json-bigint": "^1.0.4",
"@visx/responsive": "^3.12.0", "@visx/responsive": "^3.12.0",
"ace-builds": "^1.44.0", "ace-builds": "^1.44.0",
"ag-grid-community": "35.3.1", "ag-grid-community": "35.3.0",
"ag-grid-react": "35.3.1", "ag-grid-react": "35.3.0",
"brace": "^0.11.1", "brace": "^0.11.1",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"core-js": "^3.49.0", "core-js": "^3.49.0",
@@ -43,7 +43,7 @@
"d3-time": "^3.1.0", "d3-time": "^3.1.0",
"d3-time-format": "^4.1.0", "d3-time-format": "^4.1.0",
"dayjs": "^1.11.21", "dayjs": "^1.11.21",
"dompurify": "^3.4.8", "dompurify": "^3.4.7",
"fetch-retry": "^6.0.0", "fetch-retry": "^6.0.0",
"handlebars": "^4.7.9", "handlebars": "^4.7.9",
"jed": "^1.1.1", "jed": "^1.1.1",
@@ -52,12 +52,12 @@
"parse-ms": "^4.0.0", "parse-ms": "^4.0.0",
"re-resizable": "^6.11.2", "re-resizable": "^6.11.2",
"react-ace": "^14.0.1", "react-ace": "^14.0.1",
"react-draggable": "^4.6.0", "react-draggable": "^4.5.0",
"react-error-boundary": "6.0.0", "react-error-boundary": "6.0.0",
"react-js-cron": "^5.2.0", "react-js-cron": "^5.2.0",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-resize-detector": "^7.1.2", "react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^16.1.1", "react-syntax-highlighter": "^16.1.0",
"react-ultimate-pagination": "^1.3.2", "react-ultimate-pagination": "^1.3.2",
"regenerator-runtime": "^0.14.1", "regenerator-runtime": "^0.14.1",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
@@ -75,9 +75,9 @@
"@types/d3-scale": "^2.1.1", "@types/d3-scale": "^2.1.1",
"@types/d3-time": "^3.0.4", "@types/d3-time": "^3.0.4",
"@types/d3-time-format": "^4.0.3", "@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.1", "@types/jquery": "^4.0.0",
"@types/lodash": "^4.17.24", "@types/lodash": "^4.17.24",
"@types/node": "^25.9.2", "@types/node": "^25.9.1",
"@types/prop-types": "^15.7.15", "@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20", "@types/react-table": "^7.7.20",

View File

@@ -72,7 +72,6 @@ test('should generate a 2x2 grid for metrics mode', () => {
createAdhocMetric('Revenue'), createAdhocMetric('Revenue'),
createSqlMetric('Q1', 'SUM(CASE WHEN quarter = 1 THEN value END)'), createSqlMetric('Q1', 'SUM(CASE WHEN quarter = 1 THEN value END)'),
]); ]);
expect(firstCell!.formData.metric).toEqual(createAdhocMetric('Revenue'));
}); });
test('should generate grid for dimensions mode', () => { test('should generate grid for dimensions mode', () => {
@@ -214,9 +213,6 @@ test('should skip missing column metrics when generating cell form data', () =>
expect(grid!.cells[0][0]!.formData.metrics).toEqual([ expect(grid!.cells[0][0]!.formData.metrics).toEqual([
createAdhocMetric('Revenue'), createAdhocMetric('Revenue'),
]); ]);
expect(grid!.cells[0][0]!.formData.metric).toEqual(
createAdhocMetric('Revenue'),
);
}); });
test('should not escape HTML entities in cell titles', () => { test('should not escape HTML entities in cell titles', () => {
@@ -475,51 +471,6 @@ test('should handle metrics without labels', () => {
expect(grid!.colHeaders).toEqual(['count']); expect(grid!.colHeaders).toEqual(['count']);
}); });
test('should set singular metric for singular-metric chart types like Pie', () => {
const rowMetricFormData: TestFormData = {
viz_type: 'pie',
datasource: '1__table',
matrixify_enable: true,
matrixify_mode_rows: 'metrics',
matrixify_rows: [createAdhocMetric('Revenue'), createAdhocMetric('Profit')],
};
const grid = generateMatrixifyGrid(rowMetricFormData);
expect(grid).not.toBeNull();
expect(grid!.cells[0][0]!.formData.metrics).toEqual([
createAdhocMetric('Revenue'),
]);
expect(grid!.cells[0][0]!.formData.metric).toEqual(
createAdhocMetric('Revenue'),
);
expect(grid!.cells[1][0]!.formData.metrics).toEqual([
createAdhocMetric('Profit'),
]);
expect(grid!.cells[1][0]!.formData.metric).toEqual(
createAdhocMetric('Profit'),
);
});
test('should not overwrite singular metric in dimension-only mode', () => {
const dimensionFormData: TestFormData = {
viz_type: 'pie',
datasource: '1__table',
matrixify_enable: true,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_rows: {
dimension: 'country',
values: ['USA', 'Canada'],
},
metric: 'existing_metric',
};
const grid = generateMatrixifyGrid(dimensionFormData);
expect(grid).not.toBeNull();
expect(grid!.cells[0][0]!.formData.metric).toBe('existing_metric');
});
test('should preserve slice_id and dashboardId for embedded dashboard permissions', () => { test('should preserve slice_id and dashboardId for embedded dashboard permissions', () => {
const formDataWithDashboardContext: TestFormData = { const formDataWithDashboardContext: TestFormData = {
...baseFormData, ...baseFormData,

View File

@@ -197,7 +197,6 @@ function generateCellFormData(
// If we have metrics from the matrix, use them; otherwise keep original // If we have metrics from the matrix, use them; otherwise keep original
if (metrics.length > 0) { if (metrics.length > 0) {
cellFormData.metrics = metrics; cellFormData.metrics = metrics;
cellFormData.metric = metrics[0];
} }
return cellFormData; return cellFormData;

View File

@@ -17,20 +17,8 @@
* under the License. * under the License.
*/ */
import { // eslint-disable-next-line no-restricted-syntax -- whole React import is required for `reactify.test.tsx` Jest test passing.
forwardRef, import { Component, ComponentClass, WeakValidationMap } from 'react';
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
} from 'react';
import type {
ComponentType,
WeakValidationMap,
ForwardRefExoticComponent,
PropsWithoutRef,
RefAttributes,
} from 'react';
// TODO: Note that id and className can collide between Props and ReactifyProps // TODO: Note that id and className can collide between Props and ReactifyProps
// leading to (likely) unexpected behaviors. We should either require Props to not // leading to (likely) unexpected behaviors. We should either require Props to not
@@ -61,103 +49,66 @@ export interface RenderFuncType<Props> {
propTypes?: WeakValidationMap<Props & ReactifyProps>; propTypes?: WeakValidationMap<Props & ReactifyProps>;
} }
export interface ReactifiedComponentRef {
container?: HTMLDivElement;
}
export type ReactifiedComponent<Props> = ForwardRefExoticComponent<
PropsWithoutRef<Props & ReactifyProps> & RefAttributes<ReactifiedComponentRef>
>;
// Return the widest public type that covers "use it as a React component" so
// TypeScript JSX callers and `ComponentType<...>`-typed variables still compile;
// callers with explicit `ComponentClass<...>` annotations must widen to
// `ComponentType`. Those wanting the forwardRef surface can narrow to
// `ReactifiedComponent<Props>` explicitly.
export default function reactify<Props extends object>( export default function reactify<Props extends object>(
renderFn: RenderFuncType<Props>, renderFn: RenderFuncType<Props>,
callbacks?: LifeCycleCallbacks, callbacks?: LifeCycleCallbacks,
): ComponentType<Props & ReactifyProps> { ): ComponentClass<Props & ReactifyProps> {
const ReactifiedComponent = forwardRef< class ReactifiedComponent extends Component<Props & ReactifyProps> {
ReactifiedComponentRef, container?: HTMLDivElement;
Props & ReactifyProps
>(function ReactifiedComponent(props, ref) {
const containerRef = useRef<HTMLDivElement>(null);
// Keep the latest props available to the unmount callback — legacy
// consumers read values off `this.props` (e.g. ReactNVD3 uses id).
// Update the ref in a layout effect rather than during render so the
// assignment only happens for committed renders (safe under Concurrent
// Mode) and is in place before the passive unmount effect reads it.
const propsRef = useRef(props);
useLayoutEffect(() => {
propsRef.current = props;
});
// Expose container via ref for external access constructor(props: Props & ReactifyProps) {
useImperativeHandle( super(props);
ref, this.setContainerRef = this.setContainerRef.bind(this);
() => ({ }
get container() {
return containerRef.current ?? undefined;
},
}),
[],
);
// Execute renderFn on mount and every update (mimics componentDidMount + componentDidUpdate) componentDidMount() {
useEffect(() => { this.execute();
if (containerRef.current) { }
// `forwardRef` widens the props parameter to `PropsWithoutRef<...>`,
// which TypeScript can't narrow back to `Props & ReactifyProps` when componentDidUpdate() {
// `Props` is a generic `object`. The values are identical at runtime, this.execute();
// so assert the original prop shape for `renderFn`. }
renderFn(
containerRef.current, componentWillUnmount() {
props as Readonly<Props & ReactifyProps>, this.container = undefined;
); if (callbacks?.componentWillUnmount) {
callbacks.componentWillUnmount.bind(this)();
} }
}); }
// Cleanup on unmount setContainerRef(ref: HTMLDivElement) {
useEffect( this.container = ref;
() => () => { }
if (callbacks?.componentWillUnmount) {
// Preserve legacy behavior where `this` was a component instance
// exposing `props`. The class version cleared `this.container`
// before invoking componentWillUnmount, so mirror that here to
// prevent callbacks from touching a DOM node that's being torn
// down.
callbacks.componentWillUnmount.call({
container: undefined,
props: propsRef.current,
});
}
},
[],
);
const { id, className } = props; execute() {
if (this.container) {
renderFn(this.container, this.props);
}
}
return <div ref={containerRef} id={id} className={className} />; render() {
}); const { id, className } = this.props;
if (renderFn.displayName) { return <div ref={this.setContainerRef} id={id} className={className} />;
ReactifiedComponent.displayName = renderFn.displayName; }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- forwardRef static field types don't line up with renderFn's validator types const ReactifiedClass: ComponentClass<Props & ReactifyProps> =
const result = ReactifiedComponent as any; ReactifiedComponent;
if (renderFn.displayName) {
ReactifiedClass.displayName = renderFn.displayName;
}
// eslint-disable-next-line react/forbid-foreign-prop-types
if (renderFn.propTypes) { if (renderFn.propTypes) {
result.propTypes = { ReactifiedClass.propTypes = {
...result.propTypes, ...ReactifiedClass.propTypes,
...renderFn.propTypes, ...renderFn.propTypes,
}; };
} }
if (renderFn.defaultProps) { if (renderFn.defaultProps) {
result.defaultProps = renderFn.defaultProps; ReactifiedClass.defaultProps = renderFn.defaultProps;
} }
return result as unknown as ComponentType<Props & ReactifyProps>; return ReactifiedComponent;
} }

View File

@@ -18,7 +18,6 @@
*/ */
import { createRef } from 'react'; import { createRef } from 'react';
import { render, screen, waitFor } from '@superset-ui/core/spec'; import { render, screen, waitFor } from '@superset-ui/core/spec';
import { supersetTheme } from '@apache-superset/core/theme';
import type AceEditor from 'react-ace'; import type AceEditor from 'react-ace';
import { import {
AsyncAceEditor, AsyncAceEditor,
@@ -29,7 +28,6 @@ import {
CssEditor, CssEditor,
JsonEditor, JsonEditor,
ConfigEditor, ConfigEditor,
aceCompletionHighlightStyles,
} from '.'; } from '.';
import type { AceModule, AsyncAceEditorOptions } from './types'; import type { AceModule, AsyncAceEditorOptions } from './types';
@@ -44,17 +42,6 @@ test('renders SQLEditor', async () => {
}); });
}); });
test('themes the autocomplete completion highlight from the theme', () => {
// Ace ships a hardcoded `color: #000` for the matched-prefix highlight, which
// is invisible on the dark autocomplete popup. The shared editor overrides it
// from the theme so every Ace editor (SQL Lab, Explore Custom SQL, ...) stays
// consistent.
const { styles } = aceCompletionHighlightStyles(supersetTheme);
expect(styles).toContain('.ace_completion-highlight');
expect(styles).toContain(supersetTheme.colorPrimaryText);
});
test('SQLEditor uses fontFamilyCode from theme', async () => { test('SQLEditor uses fontFamilyCode from theme', async () => {
const ref = createRef<AceEditor>(); const ref = createRef<AceEditor>();
const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />); const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />);

View File

@@ -32,7 +32,7 @@ import {
AsyncEsmComponent, AsyncEsmComponent,
PlaceholderProps, PlaceholderProps,
} from '@superset-ui/core/components/AsyncEsmComponent'; } from '@superset-ui/core/components/AsyncEsmComponent';
import { useTheme, css, type SupersetTheme } from '@apache-superset/core/theme'; import { useTheme, css } from '@apache-superset/core/theme';
import { Global } from '@emotion/react'; import { Global } from '@emotion/react';
export { getTooltipHTML } from './Tooltip'; export { getTooltipHTML } from './Tooltip';
@@ -105,19 +105,6 @@ export type AsyncAceEditorOptions = {
> | null; > | null;
}; };
/**
* Theme-aware styling for the matched-prefix highlight in the autocomplete
* popup. Ace ships a hardcoded `color: #000` that is invisible on the dark
* popup, so the override needs `!important` to win. Lives in the shared editor
* so every Ace editor (SQL Lab, Explore Custom SQL, ...) stays consistent.
*/
export const aceCompletionHighlightStyles = (token: SupersetTheme) => css`
.ace_completion-highlight {
color: ${token.colorPrimaryText} !important;
background-color: ${token.colorPrimaryBgHover};
}
`;
/** /**
* Get an async AceEditor with automatical loading of specified ace modules. * Get an async AceEditor with automatical loading of specified ace modules.
*/ */
@@ -383,8 +370,6 @@ export function AsyncAceEditor(
display: flex !important; display: flex !important;
} }
${aceCompletionHighlightStyles(token)}
&&& .tooltip-detail { &&& .tooltip-detail {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import { useState } from 'react'; import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-webpack5'; import type { Meta, StoryObj } from '@storybook/react';
import { AutoComplete } from '.'; import { AutoComplete } from '.';
import type { AutoCompleteProps } from './types'; import type { AutoCompleteProps } from './types';

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import type { Meta, StoryObj } from '@storybook/react-webpack5'; import type { Meta, StoryObj } from '@storybook/react';
import { Breadcrumb } from '.'; import { Breadcrumb } from '.';
import type { BreadcrumbProps } from './types'; import type { BreadcrumbProps } from './types';

View File

@@ -16,8 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { action } from 'storybook/actions'; import { action } from '@storybook/addon-actions';
import { Meta, StoryFn } from '@storybook/react-webpack5'; import { Meta, StoryFn } from '@storybook/react';
import { CachedLabel } from '.'; import { CachedLabel } from '.';
import type { CacheLabelProps } from './types'; import type { CacheLabelProps } from './types';

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { useArgs } from 'storybook/preview-api'; import { useArgs } from '@storybook/preview-api';
import { useState } from 'react'; import { useState } from 'react';
import { Checkbox } from '.'; import { Checkbox } from '.';
import type { CheckboxProps, CheckboxChangeEvent } from './types'; import type { CheckboxProps, CheckboxChangeEvent } from './types';

View File

@@ -51,8 +51,18 @@ test('renders children with custom horizontal spacing', () => {
expect(screen.getByTestId('container')).toHaveStyle('gap: 20px'); expect(screen.getByTestId('container')).toHaveStyle('gap: 20px');
}); });
test('does not render a dropdown button when not overflowing', () => { test('renders dropdown button when items exist even when not overflowing', () => {
render(<DropdownContainer items={generateItems(3)} />); render(<DropdownContainer items={generateItems(3)} />);
// Button should always be visible when items exist to prevent layout shifts
expect(screen.getByText('More')).toBeInTheDocument();
// Badge should show 0 when nothing is overflowing
expect(screen.getByText('0')).toBeInTheDocument();
// Button is disabled when there is nothing to open, so it can't reveal an empty popover
expect(screen.getByTestId('dropdown-container-btn')).toBeDisabled();
});
test('does not render a dropdown button when no items', () => {
render(<DropdownContainer items={[]} />);
expect(screen.queryByText('More')).not.toBeInTheDocument(); expect(screen.queryByText('More')).not.toBeInTheDocument();
}); });

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