mirror of
https://github.com/apache/superset.git
synced 2026-06-10 18:19:28 +00:00
Compare commits
51 Commits
enxdev/cha
...
feat/sessi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6234ed3ada | ||
|
|
7ce5ba3fd6 | ||
|
|
ad65488939 | ||
|
|
1f81acd9e9 | ||
|
|
8a800f8f99 | ||
|
|
262b211dd6 | ||
|
|
c779a868f0 | ||
|
|
f2caeeb063 | ||
|
|
a524119888 | ||
|
|
4100adbfc1 | ||
|
|
6d1907e98f | ||
|
|
1bfdb19e88 | ||
|
|
c0e78f39d7 | ||
|
|
d51753dfdc | ||
|
|
543ad04ca0 | ||
|
|
00e3682aaf | ||
|
|
004101a752 | ||
|
|
568f34d6d8 | ||
|
|
a0cf798409 | ||
|
|
88ea96d417 | ||
|
|
c88438ad35 | ||
|
|
76f334f252 | ||
|
|
ab0fa5c3c8 | ||
|
|
9b4aaaa080 | ||
|
|
eeaa213475 | ||
|
|
2d1b17d1ca | ||
|
|
ff4783f1e4 | ||
|
|
f9ba11961a | ||
|
|
8117488fd8 | ||
|
|
336384bc67 | ||
|
|
065578e48a | ||
|
|
3949089438 | ||
|
|
efa88b9b7f | ||
|
|
f51736437d | ||
|
|
6311e2c315 | ||
|
|
7a3b8f49c7 | ||
|
|
17fb7a7c75 | ||
|
|
bf9ad4d2ba | ||
|
|
6681ab571d | ||
|
|
58d29e0779 | ||
|
|
0133ebc9f2 | ||
|
|
b64dd4af4a | ||
|
|
95d46073cb | ||
|
|
7b1e1e5668 | ||
|
|
62084f4015 | ||
|
|
f70cd8b5b8 | ||
|
|
a32b7b1523 | ||
|
|
9105adc67b | ||
|
|
443fd7bcee | ||
|
|
3259a4a781 | ||
|
|
56c856e802 |
3
.github/workflows/bump-python-package.yml
vendored
3
.github/workflows/bump-python-package.yml
vendored
@@ -30,9 +30,8 @@ jobs:
|
||||
pull-requests: write
|
||||
checks: write
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: master
|
||||
|
||||
2
.github/workflows/check-python-deps.yml
vendored
2
.github/workflows/check-python-deps.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check and notify
|
||||
|
||||
20
.github/workflows/claude.yml
vendored
20
.github/workflows/claude.yml
vendored
@@ -75,14 +75,14 @@ jobs:
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@5fb899572b81d2bb648d4d187173a2f423a9677c # beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@5fb899572b81d2bb648d4d187173a2f423a9677c # beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -57,13 +57,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
|
||||
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# 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
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
|
||||
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
4
.github/workflows/dependency-review.yml
vendored
4
.github/workflows/dependency-review.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Dependency Review"
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
@@ -18,7 +18,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
changes:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
@@ -31,7 +30,7 @@ jobs:
|
||||
docker: ${{ steps.check.outputs.docker }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -71,9 +70,8 @@ jobs:
|
||||
IMAGE_TAG: apache/superset:GHA-${{ matrix.build_preset }}-${{ github.run_id }}
|
||||
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -147,7 +145,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Docker Environment
|
||||
|
||||
6
.github/workflows/embedded-sdk-release.yml
vendored
6
.github/workflows/embedded-sdk-release.yml
vendored
@@ -33,13 +33,13 @@ jobs:
|
||||
run:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: "./superset-embedded-sdk/.nvmrc"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm ci
|
||||
- run: npm run ci:release
|
||||
env:
|
||||
|
||||
6
.github/workflows/embedded-sdk-test.yml
vendored
6
.github/workflows/embedded-sdk-test.yml
vendored
@@ -21,13 +21,13 @@ jobs:
|
||||
run:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: "./superset-embedded-sdk/.nvmrc"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
|
||||
2
.github/workflows/generate-FOSSA-report.yml
vendored
2
.github/workflows/generate-FOSSA-report.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -18,7 +18,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
validate-all-ghas:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
@@ -28,14 +27,14 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install -g @action-validator/core @action-validator/cli --save-dev
|
||||
|
||||
3
.github/workflows/issue_creation.yml
vendored
3
.github/workflows/issue_creation.yml
vendored
@@ -15,9 +15,8 @@ jobs:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
46
.github/workflows/latest-release-tag.yml
vendored
46
.github/workflows/latest-release-tag.yml
vendored
@@ -11,29 +11,29 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Check for latest tag
|
||||
id: latest-tag
|
||||
env:
|
||||
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
source ./scripts/tag_latest_release.sh "$RELEASE_TAG_NAME" --dry-run
|
||||
- name: Check for latest tag
|
||||
id: latest-tag
|
||||
env:
|
||||
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
source ./scripts/tag_latest_release.sh "$RELEASE_TAG_NAME" --dry-run
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Run latest-tag
|
||||
uses: ./.github/actions/latest-tag
|
||||
if: steps.latest-tag.outputs.SKIP_TAG != 'true'
|
||||
with:
|
||||
description: Superset latest release
|
||||
tag-name: latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
- name: Run latest-tag
|
||||
uses: ./.github/actions/latest-tag
|
||||
if: steps.latest-tag.outputs.SKIP_TAG != 'true'
|
||||
with:
|
||||
description: Superset latest release
|
||||
tag-name: latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
6
.github/workflows/license-check.yml
vendored
6
.github/workflows/license-check.yml
vendored
@@ -18,14 +18,14 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '11'
|
||||
distribution: "temurin"
|
||||
java-version: "11"
|
||||
- name: Run license check
|
||||
run: ./scripts/check_license.sh
|
||||
|
||||
5
.github/workflows/pr-lint.yml
vendored
5
.github/workflows/pr-lint.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -31,6 +31,5 @@ jobs:
|
||||
on-failed-regex-fail-action: true
|
||||
on-failed-regex-request-changes: 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 }}"
|
||||
|
||||
10
.github/workflows/pre-commit.yml
vendored
10
.github/workflows/pre-commit.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -48,9 +48,9 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'superset-frontend/package-lock.json'
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "superset-frontend/package-lock.json"
|
||||
|
||||
- name: Install Frontend Dependencies
|
||||
run: |
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
id: changed_files
|
||||
uses: ./.github/actions/file-changes-action
|
||||
with:
|
||||
output: ' '
|
||||
output: " "
|
||||
|
||||
- name: pre-commit
|
||||
env:
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
# pulls all commits (needed for lerna / semantic release to correctly version)
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
if: env.HAS_TAGS
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
node-version-file: "./superset-frontend/.nvmrc"
|
||||
|
||||
- name: Cache npm
|
||||
if: env.HAS_TAGS
|
||||
|
||||
6
.github/workflows/showtime-trigger.yml
vendored
6
.github/workflows/showtime-trigger.yml
vendored
@@ -10,11 +10,11 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to sync'
|
||||
description: "PR number to sync"
|
||||
required: true
|
||||
type: number
|
||||
sha:
|
||||
description: 'Specific SHA to deploy (optional, defaults to latest)'
|
||||
description: "Specific SHA to deploy (optional, defaults to latest)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
|
||||
- name: Checkout PR code (only if build needed)
|
||||
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ steps.check.outputs.target_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
2
.github/workflows/superset-app-cli.yml
vendored
2
.github/workflows/superset-app-cli.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
8
.github/workflows/superset-docs-deploy.yml
vendored
8
.github/workflows/superset-docs-deploy.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
persist-credentials: false
|
||||
@@ -68,13 +68,13 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '21'
|
||||
distribution: "zulu"
|
||||
java-version: "21"
|
||||
- name: Install Graphviz
|
||||
run: sudo apt-get install -y graphviz
|
||||
- name: Compute Entity Relationship diagram (ERD)
|
||||
|
||||
14
.github/workflows/superset-docs-verify.yml
vendored
14
.github/workflows/superset-docs-verify.yml
vendored
@@ -28,12 +28,12 @@ jobs:
|
||||
name: Link Checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Do not bump this linkinator-action version without opening
|
||||
# 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)
|
||||
with:
|
||||
paths: "**/*.md, **/*.mdx"
|
||||
@@ -73,14 +73,14 @@ jobs:
|
||||
working-directory: docs
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
working-directory: docs
|
||||
steps:
|
||||
- name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
persist-credentials: false
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: database-diagnostics
|
||||
path: docs/src/data/
|
||||
if_no_artifact_found: 'warning'
|
||||
if_no_artifact_found: "warning"
|
||||
- name: Use fresh diagnostics
|
||||
run: |
|
||||
if [ -f "src/data/databases-diagnostics.json" ]; then
|
||||
|
||||
38
.github/workflows/superset-e2e.yml
vendored
38
.github/workflows/superset-e2e.yml
vendored
@@ -10,17 +10,17 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
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
|
||||
default: 'false'
|
||||
default: "false"
|
||||
ref:
|
||||
description: 'The branch or tag to checkout'
|
||||
description: "The branch or tag to checkout"
|
||||
required: false
|
||||
default: ''
|
||||
default: ""
|
||||
pr_id:
|
||||
description: 'The pull request ID to checkout'
|
||||
description: "The pull request ID to checkout"
|
||||
required: false
|
||||
default: ''
|
||||
default: ""
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -97,21 +97,21 @@ jobs:
|
||||
# Conditional checkout based on context
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
@@ -130,9 +130,9 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'superset-frontend/package-lock.json'
|
||||
node-version-file: "./superset-frontend/.nvmrc"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "superset-frontend/package-lock.json"
|
||||
- name: Install npm dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
@@ -207,21 +207,21 @@ jobs:
|
||||
# Conditional checkout based on context (same as Cypress workflow)
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
@@ -240,9 +240,9 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'superset-frontend/package-lock.json'
|
||||
node-version-file: "./superset-frontend/.nvmrc"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "superset-frontend/package-lock.json"
|
||||
- name: Install npm dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
working-directory: superset-extensions-cli
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
4
.github/workflows/superset-frontend.yml
vendored
4
.github/workflows/superset-frontend.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
should-run: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
4
.github/workflows/superset-helm-lint.yml
vendored
4
.github/workflows/superset-helm-lint.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
with:
|
||||
install-superset: 'false'
|
||||
install-superset: "false"
|
||||
|
||||
- name: Set up chart-testing
|
||||
uses: ./.github/actions/chart-testing-action
|
||||
|
||||
2
.github/workflows/superset-helm-release.yml
vendored
2
.github/workflows/superset-helm-release.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref_name }}
|
||||
persist-credentials: true
|
||||
|
||||
22
.github/workflows/superset-playwright.yml
vendored
22
.github/workflows/superset-playwright.yml
vendored
@@ -10,13 +10,13 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'The branch or tag to checkout'
|
||||
description: "The branch or tag to checkout"
|
||||
required: false
|
||||
default: ''
|
||||
default: ""
|
||||
pr_id:
|
||||
description: 'The pull request ID to checkout'
|
||||
description: "The pull request ID to checkout"
|
||||
required: false
|
||||
default: ''
|
||||
default: ""
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -83,21 +83,21 @@ jobs:
|
||||
# Conditional checkout based on context (same as Cypress workflow)
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
@@ -116,9 +116,9 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'superset-frontend/package-lock.json'
|
||||
node-version-file: "./superset-frontend/.nvmrc"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "superset-frontend/package-lock.json"
|
||||
- name: Install npm dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
10
.github/workflows/superset-translations.yml
vendored
10
.github/workflows/superset-translations.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -40,9 +40,9 @@ jobs:
|
||||
if: steps.check.outputs.frontend
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'superset-frontend/package-lock.json'
|
||||
node-version-file: "./superset-frontend/.nvmrc"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "superset-frontend/package-lock.json"
|
||||
- name: Install dependencies
|
||||
if: steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/superset-websocket.yml
vendored
2
.github/workflows/superset-websocket.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install dependencies
|
||||
|
||||
4
.github/workflows/supersetbot.yml
vendored
4
.github/workflows/supersetbot.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
comment_body:
|
||||
description: 'Comment Body'
|
||||
description: "Comment Body"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
});
|
||||
|
||||
- name: "Checkout ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
15
.github/workflows/tag-release.yml
vendored
15
.github/workflows/tag-release.yml
vendored
@@ -16,11 +16,11 @@ on:
|
||||
force-latest:
|
||||
required: true
|
||||
type: choice
|
||||
default: 'false'
|
||||
default: "false"
|
||||
description: Whether to force a latest tag on the release
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
- "true"
|
||||
- "false"
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -49,12 +49,12 @@ jobs:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
build_preset: ["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
|
||||
build_preset:
|
||||
["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
|
||||
fail-fast: false
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -119,9 +119,8 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
4
.github/workflows/tech-debt.yml
vendored
4
.github/workflows/tech-debt.yml
vendored
@@ -32,14 +32,14 @@ jobs:
|
||||
name: Generate Reports
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
node-version-file: "./superset-frontend/.nvmrc"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -58,6 +58,10 @@ 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.
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.60.0",
|
||||
"@typescript-eslint/parser": "^8.60.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
@@ -109,7 +109,7 @@
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.60.0",
|
||||
"typescript-eslint": "^8.60.1",
|
||||
"webpack": "^5.107.2"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
156
docs/yarn.lock
156
docs/yarn.lock
@@ -4812,110 +4812,110 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.60.0", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz#8fc1e0a950c43270eaf0212dc060f7edaa42f9cf"
|
||||
integrity sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==
|
||||
"@typescript-eslint/eslint-plugin@8.60.1", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz#c1060bb8fa4be80624d3f3dec8dd9caca373af76"
|
||||
integrity sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/type-utils" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
"@typescript-eslint/scope-manager" "8.60.1"
|
||||
"@typescript-eslint/type-utils" "8.60.1"
|
||||
"@typescript-eslint/utils" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.60.0", "@typescript-eslint/parser@^8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.0.tgz#38d611b8e658cb10850d4975e8a175a222fbcd6a"
|
||||
integrity sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==
|
||||
"@typescript-eslint/parser@8.60.1", "@typescript-eslint/parser@^8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289"
|
||||
integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
"@typescript-eslint/scope-manager" "8.60.1"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.0.tgz#b82ab12e64d005d0c7163d1240c432381f1bde0f"
|
||||
integrity sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==
|
||||
"@typescript-eslint/project-service@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz#eb29712f58d72c222fc727162e92f2ab4670971b"
|
||||
integrity sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.60.0"
|
||||
"@typescript-eslint/types" "^8.60.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.60.1"
|
||||
"@typescript-eslint/types" "^8.60.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz#7617a4617c043fe235dcf066f9a40f106cfd2fd5"
|
||||
integrity sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==
|
||||
"@typescript-eslint/scope-manager@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz#2f875962eaad0a0789cc3c36aea9b4ddeb2dd9c8"
|
||||
integrity sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "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":
|
||||
"@typescript-eslint/tsconfig-utils@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
|
||||
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
|
||||
|
||||
"@typescript-eslint/type-utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz#6971a61bc4f3a1b2df45dcc14e26a43a88a4cb6a"
|
||||
integrity sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==
|
||||
"@typescript-eslint/tsconfig-utils@^8.60.1":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba"
|
||||
integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==
|
||||
|
||||
"@typescript-eslint/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:
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/utils" "8.60.1"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@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":
|
||||
"@typescript-eslint/types@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4"
|
||||
integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz#c102196a44414481190041c99eea1d854e66001b"
|
||||
integrity sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==
|
||||
"@typescript-eslint/types@^8.60.1":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d"
|
||||
integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==
|
||||
|
||||
"@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:
|
||||
"@typescript-eslint/project-service" "8.60.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
"@typescript-eslint/project-service" "8.60.1"
|
||||
"@typescript-eslint/tsconfig-utils" "8.60.1"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
debug "^4.4.3"
|
||||
minimatch "^10.2.2"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.0.tgz#6110cddaef87606ae4ca6f8bf81bb5949fc8e098"
|
||||
integrity sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==
|
||||
"@typescript-eslint/utils@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.1.tgz#31cf566095602d9fe8ad91837d2eb520b8de762b"
|
||||
integrity sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/scope-manager" "8.60.1"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz#f2c41eedd3d7b03b808369fb2e3fb40a93783ec2"
|
||||
integrity sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==
|
||||
"@typescript-eslint/visitor-keys@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz#165d1d8901137b944efaf18f00ab5ecb57f06995"
|
||||
integrity sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -13490,9 +13490,9 @@ shebang-regex@^3.0.0:
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shell-quote@^1.8.3:
|
||||
version "1.8.3"
|
||||
resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz"
|
||||
integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
|
||||
version "1.8.4"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190"
|
||||
integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==
|
||||
|
||||
shelljs@0.8.5:
|
||||
version "0.8.5"
|
||||
@@ -14389,15 +14389,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.60.0:
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.0.tgz#6686fecb1f4f367c0bf0075828e93b7ecacbc62b"
|
||||
integrity sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==
|
||||
typescript-eslint@^8.60.1:
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.1.tgz#13db05c6eabb89669deec44545b788a0e9aee640"
|
||||
integrity sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.60.0"
|
||||
"@typescript-eslint/parser" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.60.1"
|
||||
"@typescript-eslint/parser" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/utils" "8.60.1"
|
||||
|
||||
typescript@~6.0.3:
|
||||
version "6.0.3"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
apiVersion: v2
|
||||
appVersion: "5.0.0"
|
||||
appVersion: "6.1.0"
|
||||
description: Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
name: superset
|
||||
icon: https://artifacthub.io/image/68c1d717-0e97-491f-b046-754e46f46922@2x
|
||||
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.15.5 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.16.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ dependencies = [
|
||||
"python-dateutil",
|
||||
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
|
||||
"pygeohash",
|
||||
"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
|
||||
"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
|
||||
"pyyaml>=6.0.0, <7.0.0",
|
||||
"PyJWT>=2.4.0, <3.0",
|
||||
"redis>=5.0.0, <6.0",
|
||||
@@ -447,6 +447,7 @@ requirement_txt_file = "requirements/base.txt"
|
||||
authorized_licenses = [
|
||||
"academic free license (afl)",
|
||||
"any-osi",
|
||||
"apache-2.0",
|
||||
"apache license 2.0",
|
||||
"apache software",
|
||||
"apache software, bsd",
|
||||
|
||||
@@ -30,7 +30,7 @@ cryptography>=46.0.7,<47.0.0
|
||||
# Security: Snyk - XSS vulnerability in Mako templates
|
||||
mako>=1.3.11,<2.0.0
|
||||
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers
|
||||
pyarrow>=20.0.0,<21.0.0
|
||||
pyarrow>=24.0.0,<25.0.0
|
||||
# Security: CVE-2026-27459 - pyopenssl certificate validation
|
||||
pyopenssl>=26.0.0,<27.0.0
|
||||
# Security: CVE-2026-25645 (MEDIUM) - Insecure Temporary File
|
||||
|
||||
@@ -294,7 +294,7 @@ prison==0.2.1
|
||||
# via flask-appbuilder
|
||||
prompt-toolkit==3.0.51
|
||||
# via click-repl
|
||||
pyarrow==20.0.0
|
||||
pyarrow==24.0.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# apache-superset (pyproject.toml)
|
||||
|
||||
@@ -715,7 +715,7 @@ psycopg2-binary==2.9.12
|
||||
# via apache-superset
|
||||
py-key-value-aio==0.4.4
|
||||
# via fastmcp
|
||||
pyarrow==20.0.0
|
||||
pyarrow==24.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -55,10 +55,21 @@ msgcat --sort-by-msgid --no-wrap --no-location superset/translations/messages.po
|
||||
cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \
|
||||
&& 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 \
|
||||
-i superset/translations/messages.pot \
|
||||
-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
|
||||
for file in $( find superset/translations/** );
|
||||
|
||||
@@ -20,20 +20,21 @@ Check that source-code changes don't cause translation regressions.
|
||||
|
||||
What counts as a regression
|
||||
---------------------------
|
||||
A regression is an *existing translation that a source change invalidated* —
|
||||
i.e. a string was renamed/reworded so its committed translation no longer
|
||||
applies. ``babel_update.sh`` (``pybabel update --ignore-obsolete``) surfaces
|
||||
exactly these as **newly fuzzy** entries: the old translation is fuzzy-matched
|
||||
onto the new ``msgid`` and flagged ``#, fuzzy``.
|
||||
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
|
||||
translated count, because a count drop happens identically for a benign
|
||||
*deletion* and a real *rename*, so it cannot distinguish the two — whereas a
|
||||
``#, fuzzy`` marker unambiguously flags a stranded translation.
|
||||
|
||||
Crucially, *deleting* a translatable string is **not** a regression. With
|
||||
``--ignore-obsolete`` a removed string is dropped from the catalogs entirely;
|
||||
no fuzzy entry is created. So a PR that intentionally removes a string (e.g. a
|
||||
security fix that stops rendering a value) legitimately lowers the translated
|
||||
count without introducing any fuzzies, and must not be flagged. We therefore
|
||||
key the check on the **increase in fuzzy entries**, not on a drop in the
|
||||
translated count (a drop happens identically for a benign deletion and a real
|
||||
rename, so it cannot distinguish the two).
|
||||
Note ``babel_update.sh`` runs ``pybabel update`` with ``--no-fuzzy-matching``,
|
||||
so *adding* (or renaming) a source string does **not** auto-generate a fuzzy
|
||||
guess against an unrelated existing translation — new strings land as cleanly
|
||||
untranslated (empty ``msgstr``). This deliberately avoids the prior behaviour
|
||||
where *every* PR that merely added a translatable string tripped this check on
|
||||
spurious fuzzies. As a result the check now guards against ``#, fuzzy`` entries
|
||||
that arrive another way — e.g. a committed ``.po`` edit — rather than ones the
|
||||
update step synthesises. *Deleting* a string is still not a regression: with
|
||||
``--ignore-obsolete`` it is simply dropped and no fuzzy is created.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
@@ -69,6 +69,10 @@ class BaseExtension(BaseModel):
|
||||
default=None,
|
||||
description="Extension description",
|
||||
)
|
||||
dependencies: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="List of extension IDs this extension depends on",
|
||||
)
|
||||
permissions: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Permissions required by this extension",
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
getGuestTokenRefreshTiming,
|
||||
MIN_REFRESH_WAIT_MS,
|
||||
DEFAULT_TOKEN_EXP_MS,
|
||||
DEFAULT_TOKEN_REFRESH_RETRY_MS,
|
||||
} from "./guestTokenRefresh";
|
||||
|
||||
describe("guest token refresh", () => {
|
||||
@@ -93,4 +94,11 @@ describe("guest token refresh", () => {
|
||||
expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import { jwtDecode } from "jwt-decode";
|
||||
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 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?
|
||||
export function getGuestTokenRefreshTiming(currentGuestToken: string) {
|
||||
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
|
||||
// We can swap this out for the actual switchboard package once it gets published
|
||||
import { Switchboard } from '@superset-ui/switchboard';
|
||||
import { getGuestTokenRefreshTiming } from './guestTokenRefresh';
|
||||
import {
|
||||
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.
|
||||
@@ -49,6 +53,9 @@ export type UiConfigType = {
|
||||
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 = {
|
||||
/** The id provided by the embed configuration UI in Superset */
|
||||
id: string;
|
||||
@@ -73,6 +80,10 @@ export type EmbedDashboardParams = {
|
||||
/** 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. */
|
||||
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 = {
|
||||
@@ -127,6 +138,7 @@ export async function embedDashboard({
|
||||
iframeAllowExtras = [],
|
||||
referrerPolicy,
|
||||
resolvePermalinkUrl,
|
||||
guestTokenFetchTimeoutMs = DEFAULT_GUEST_TOKEN_FETCH_TIMEOUT_MS,
|
||||
}: EmbedDashboardParams): Promise<EmbeddedDashboard> {
|
||||
function log(...info: unknown[]) {
|
||||
if (debug) {
|
||||
@@ -134,6 +146,16 @@ 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');
|
||||
|
||||
if (supersetDomain.endsWith('/')) {
|
||||
@@ -247,21 +269,57 @@ export async function embedDashboard({
|
||||
});
|
||||
}
|
||||
|
||||
const [guestToken, ourPort]: [string, Switchboard] = await Promise.all([
|
||||
fetchGuestToken(),
|
||||
mountIframe(),
|
||||
]);
|
||||
let guestToken: string;
|
||||
let ourPort: Switchboard;
|
||||
try {
|
||||
[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 });
|
||||
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() {
|
||||
const newGuestToken = await fetchGuestToken();
|
||||
ourPort.emit('guestToken', { guestToken: newGuestToken });
|
||||
setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(newGuestToken));
|
||||
if (unmounted) return;
|
||||
try {
|
||||
const newGuestToken = await fetchGuestTokenWithTimeout();
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(guestToken));
|
||||
refreshTimer = setTimeout(
|
||||
refreshGuestToken,
|
||||
getGuestTokenRefreshTiming(guestToken),
|
||||
);
|
||||
|
||||
// Register the resolvePermalinkUrl method for the iframe to call
|
||||
// Returns null if no callback provided or on error, allowing iframe to use default URL
|
||||
@@ -283,6 +341,11 @@ export async function embedDashboard({
|
||||
|
||||
function unmount() {
|
||||
log('unmounting');
|
||||
unmounted = true;
|
||||
if (refreshTimer !== undefined) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
//@ts-ignore
|
||||
mountPoint.replaceChildren();
|
||||
}
|
||||
|
||||
@@ -16,16 +16,24 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
/**
|
||||
* View locations for app-shell extension integration.
|
||||
*
|
||||
* These define locations that persist across all routes, mirroring the `app`
|
||||
* scope of the `ViewContributions` manifest schema.
|
||||
*/
|
||||
export const AppViewLocations = {
|
||||
app: {
|
||||
chatbot: 'core.chatbot',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const CHATBOT_LOCATION = AppViewLocations.app.chatbot;
|
||||
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"
|
||||
);
|
||||
});
|
||||
43
superset-embedded-sdk/src/withTimeout.ts
Normal file
43
superset-embedded-sdk/src/withTimeout.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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>;
|
||||
}
|
||||
@@ -179,6 +179,7 @@ def build_manifest(cwd: Path, remote_entry: str | None) -> Manifest:
|
||||
displayName=extension.displayName,
|
||||
version=extension.version,
|
||||
permissions=extension.permissions,
|
||||
dependencies=extension.dependencies,
|
||||
frontend=frontend,
|
||||
backend=backend,
|
||||
)
|
||||
|
||||
@@ -283,6 +283,7 @@ def test_build_manifest_creates_correct_manifest_structure(
|
||||
"displayName": "Test Extension",
|
||||
"version": "1.0.0",
|
||||
"permissions": ["read_data"],
|
||||
"dependencies": ["some_dep"],
|
||||
}
|
||||
extension_json = isolated_filesystem / "extension.json"
|
||||
extension_json.write_text(json.dumps(extension_data))
|
||||
@@ -296,6 +297,7 @@ def test_build_manifest_creates_correct_manifest_structure(
|
||||
assert manifest.displayName == "Test Extension"
|
||||
assert manifest.version == "1.0.0"
|
||||
assert manifest.permissions == ["read_data"]
|
||||
assert manifest.dependencies == ["some_dep"]
|
||||
|
||||
# Verify frontend section
|
||||
assert manifest.frontend is not None
|
||||
@@ -328,6 +330,7 @@ def test_build_manifest_handles_minimal_extension(isolated_filesystem):
|
||||
assert manifest.displayName == "Minimal Extension"
|
||||
assert manifest.version == "0.1.0"
|
||||
assert manifest.permissions == []
|
||||
assert manifest.dependencies == [] # Default empty list
|
||||
assert manifest.frontend is None
|
||||
assert manifest.backend is None
|
||||
|
||||
|
||||
348
superset-frontend/package-lock.json
generated
348
superset-frontend/package-lock.json
generated
@@ -223,7 +223,7 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||
"@typescript-eslint/parser": "^8.59.4",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-loader": "^10.1.1",
|
||||
@@ -232,7 +232,7 @@
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"baseline-browser-mapping": "^2.10.33",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^10.0.0",
|
||||
"concurrently": "^10.0.3",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"css-loader": "^7.1.4",
|
||||
@@ -240,7 +240,7 @@
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-cypress": "^3.6.0",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
|
||||
@@ -270,7 +270,7 @@
|
||||
"lightningcss": "^1.32.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.67.0",
|
||||
"oxlint": "^1.68.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.8.3",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
@@ -8700,9 +8700,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@oxlint/binding-android-arm-eabi": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.67.0.tgz",
|
||||
"integrity": "sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.68.0.tgz",
|
||||
"integrity": "sha512-wEdsIspexXLLMCPAEOcCuFLMt6aE3AzTuA/nQKLPRnoJ+EQTturmGheDkhHuuVHx0GbutjQ3JKmEn+Gz6Ag28Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -8717,9 +8717,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-android-arm64": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.67.0.tgz",
|
||||
"integrity": "sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.68.0.tgz",
|
||||
"integrity": "sha512-6aZRNNXQTsYtgaus8HTb9nuCcsrQTlKXGnktwvwW0n/SooRWNxNb3925grDkC63aEYZuCIyOVLV16IdYIoC2aQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8734,9 +8734,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-darwin-arm64": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.67.0.tgz",
|
||||
"integrity": "sha512-jOzXxS1AxFxhImLIRbtGIMrEwaXcgMw3gR57WB1cRk8ai+vpr6726kxXqVvlNsrXtJ/FrmOm8RxlC0m8SW24Qg==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.68.0.tgz",
|
||||
"integrity": "sha512-lVTbsE3kO4bLpZELgjRZuAJc8kP98wb83yMXWH8gaPaFZ+cM2IDeZto4ByoUAYj0Mxv2rvw+A1ssZequSepVSg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8751,9 +8751,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-darwin-x64": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.67.0.tgz",
|
||||
"integrity": "sha512-3DFAVY94OqjIZHXIPz37yGRSWwOFTAqChQ64/M69GYLawzP0KiwdhDNfqdKKYT0bTR/DNxmMnQsj3ns+8+X/Lg==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.68.0.tgz",
|
||||
"integrity": "sha512-nCmw2XrmQskjBUh/sfP5yKs93V68LijQgjd1cuuZ/q4SCARngLYs60/qqyzuMsg8QQ9KArDI98hxs/RDGE4KRQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8768,9 +8768,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-freebsd-x64": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.67.0.tgz",
|
||||
"integrity": "sha512-e4dDKZuLu8TR9DEBssWSDahlPgZBwojTTHZUvnjBRJfJJbpxYCjfjKfi0Z1+CSLMiJBwI2yCDtRM1XJQaARjmg==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.68.0.tgz",
|
||||
"integrity": "sha512-TI4ovQJliYE9V6e06cEv+qEI9uj7Ao65fmif4er4HD+aouyYyh0P31q2jh3KtqsOHHcQqv2PZ61TjJFLpBDGWQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8785,9 +8785,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.67.0.tgz",
|
||||
"integrity": "sha512-BKytFdcQzbITV3xlnzDUDTEDtbUMCCiC4EaNTDZ4FyT8gdNvBC4gfiLucXp/sQl0XU3p7syTlorUWVVVBZab2g==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.68.0.tgz",
|
||||
"integrity": "sha512-LcNnEi9g71Cmry5ZpLbKT+oVv+/zYG3hYVAbBBB5X85nOQZSk8l92CnDkxJMcxUg0NCnMCOFZuaVDlMyv4tYJw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -8802,9 +8802,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.67.0.tgz",
|
||||
"integrity": "sha512-XYAv0esBDX7BpTzRDjVX2Vdj+zndd8ll2dFQiaeQ6zTZr7A8GRDTN7fH3FP3jU+O0vCDx85oH/EtG7BzPgAXuw==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.68.0.tgz",
|
||||
"integrity": "sha512-OovHahL3FX4UaK+hgSf11llUx2vszqjSdQQ61Ck9InOEI/ptZoC4XSQJurITqItVvd53JSlmkLMeaNjM1PoQew==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -8819,9 +8819,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm64-gnu": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.67.0.tgz",
|
||||
"integrity": "sha512-zizRMjA0i6u/2B0evgda04iycu+MoNuf1pBy6Eh+1CjC5wMEG7qN5zdDKTCvFc0KSYSDM9QTG3gjZHirgtQuKg==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.68.0.tgz",
|
||||
"integrity": "sha512-YbzTglnHLzzi9zv5or8Ztz5fykAoZE8W9iM42/bOrF4HBSB6rJTqdLQWuoP76EHQw9DuKl76K1QmFlG29sPJXQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8836,9 +8836,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm64-musl": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.67.0.tgz",
|
||||
"integrity": "sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.68.0.tgz",
|
||||
"integrity": "sha512-qVKtCZNic+OoNnOr/hCQAu22HSQzflI7Fsq/Blzkw02SnLuv163k3kfmrVpZjSBlUHgsRKj6WgQiw30d3SX02Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8853,9 +8853,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.67.0.tgz",
|
||||
"integrity": "sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.68.0.tgz",
|
||||
"integrity": "sha512-zExyZ8ZOUuAyQ0y9jpTcyjKUz62YY9JhKPyVxzvjTpXzZ3ujdqiVwfPWDdnA1SsIOrxdtxHn7KErDHLWskFjXg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -8870,9 +8870,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.67.0.tgz",
|
||||
"integrity": "sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.68.0.tgz",
|
||||
"integrity": "sha512-6C4MPuwewyDavA7sxM14wzgRi5GGL68HPIxRCdVyS75U4MDbpFVYzKO9WNR6KLKTMPq2pcz3THwo1sK2uiqngw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -8887,9 +8887,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-riscv64-musl": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.67.0.tgz",
|
||||
"integrity": "sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.68.0.tgz",
|
||||
"integrity": "sha512-bnZooVeHAcvA+dH0EDLgx+7HY/DRi6e0hFszg3P+OBatuUjV6EvfIyNIzWOusmqAVh4L6r21GGTZtiKE4iqM4Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -8904,9 +8904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-s390x-gnu": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.67.0.tgz",
|
||||
"integrity": "sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.68.0.tgz",
|
||||
"integrity": "sha512-dIqnZnJSmHCMOUpUcWQOiV14o3DDPVx1DSsMaSzvdhNjC1tB1iEPZbdiMSCIEYbkgbsYznHXWqFdKL8WUB3F8g==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -8921,9 +8921,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-x64-gnu": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.67.0.tgz",
|
||||
"integrity": "sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.68.0.tgz",
|
||||
"integrity": "sha512-zc9lEnfV/HreDTY6gdMlZe+irkwHSxQ4/B1pS9GyK7RVaA5LxhoZY/w6/o2vIwLLEYiXQ5ujGxOM1ZazeFAAIA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8938,9 +8938,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-x64-musl": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.67.0.tgz",
|
||||
"integrity": "sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.68.0.tgz",
|
||||
"integrity": "sha512-Dl5QEX0TCo/40Cdh1o1JdPS//+YiWqjC+Hrrya5OQmStZZr4svAFtdlqcpCrU9yq2Mo3vRVyO9B3h0dzD8s36Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8955,9 +8955,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-openharmony-arm64": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.67.0.tgz",
|
||||
"integrity": "sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.68.0.tgz",
|
||||
"integrity": "sha512-/qy6dOvi4S3/LeXq0l5BT5pRKPYA7oj3uKwJOAZOr5HRLL+HK6jdBynvWuXIA2wwfE01RzNYmbBdM7vwYx00sA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8972,9 +8972,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-arm64-msvc": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.67.0.tgz",
|
||||
"integrity": "sha512-2VhwE6Gatb0vJGnN0TBuQMbKCOiZlSQ/zJvVWYLK4a9d4iDiJOen/yVQkGpmsJ90MuH66fzi0kEKI0jRQMDxGA==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.68.0.tgz",
|
||||
"integrity": "sha512-fHNtVqPHSYE7UFDSLVFUjxQjnSVXxseNJmRW+XuP4pXXDwePdPda43NL7/BBCFTxHjycOc44JNDaOPtFDNui9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8989,9 +8989,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-ia32-msvc": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.67.0.tgz",
|
||||
"integrity": "sha512-EQ3VExXfeM1InbE5+JjufhZZTWy+kHUwgt3yZR7gQ47Je/mE0WspQPan0OJznh493L5anM210YNJtH1PXjTSFg==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.68.0.tgz",
|
||||
"integrity": "sha512-NnKXr4Wgo4nps3erhrE0f8shBvBPZMHg72nDsvX0JyrRvsNiP3f1JNvbCKh+A6VFvpF7ZoJxu904P3cKMhvZnA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -9006,9 +9006,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-x64-msvc": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.67.0.tgz",
|
||||
"integrity": "sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.68.0.tgz",
|
||||
"integrity": "sha512-zg5pA+84AlU6XHJ3ruiRxziO71QTrz8nLsk6u01JGS5+tL9/bnlakFiklFrcy4R1/V7ktWtaNitN3JZWmKnf6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -14158,17 +14158,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz",
|
||||
"integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
|
||||
"integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.60.0",
|
||||
"@typescript-eslint/type-utils": "8.60.0",
|
||||
"@typescript-eslint/utils": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0",
|
||||
"@typescript-eslint/scope-manager": "8.60.1",
|
||||
"@typescript-eslint/type-utils": "8.60.1",
|
||||
"@typescript-eslint/utils": "8.60.1",
|
||||
"@typescript-eslint/visitor-keys": "8.60.1",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -14181,20 +14181,20 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.60.0",
|
||||
"@typescript-eslint/parser": "^8.60.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz",
|
||||
"integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz",
|
||||
"integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.60.0",
|
||||
"@typescript-eslint/types": "^8.60.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.60.1",
|
||||
"@typescript-eslint/types": "^8.60.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -14209,14 +14209,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz",
|
||||
"integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz",
|
||||
"integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0"
|
||||
"@typescript-eslint/types": "8.60.1",
|
||||
"@typescript-eslint/visitor-keys": "8.60.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -14227,9 +14227,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz",
|
||||
"integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz",
|
||||
"integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14244,9 +14244,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz",
|
||||
"integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz",
|
||||
"integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14258,16 +14258,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz",
|
||||
"integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz",
|
||||
"integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.60.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0",
|
||||
"@typescript-eslint/project-service": "8.60.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.60.1",
|
||||
"@typescript-eslint/types": "8.60.1",
|
||||
"@typescript-eslint/visitor-keys": "8.60.1",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -14286,16 +14286,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz",
|
||||
"integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz",
|
||||
"integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/typescript-estree": "8.60.0"
|
||||
"@typescript-eslint/scope-manager": "8.60.1",
|
||||
"@typescript-eslint/types": "8.60.1",
|
||||
"@typescript-eslint/typescript-estree": "8.60.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -14310,13 +14310,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz",
|
||||
"integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz",
|
||||
"integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.1",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -14655,15 +14655,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz",
|
||||
"integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz",
|
||||
"integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/typescript-estree": "8.60.0",
|
||||
"@typescript-eslint/utils": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.1",
|
||||
"@typescript-eslint/typescript-estree": "8.60.1",
|
||||
"@typescript-eslint/utils": "8.60.1",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -14680,14 +14680,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz",
|
||||
"integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz",
|
||||
"integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.60.0",
|
||||
"@typescript-eslint/types": "^8.60.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.60.1",
|
||||
"@typescript-eslint/types": "^8.60.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -14702,14 +14702,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz",
|
||||
"integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz",
|
||||
"integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0"
|
||||
"@typescript-eslint/types": "8.60.1",
|
||||
"@typescript-eslint/visitor-keys": "8.60.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -14720,9 +14720,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz",
|
||||
"integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz",
|
||||
"integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14737,9 +14737,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz",
|
||||
"integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz",
|
||||
"integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14751,16 +14751,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz",
|
||||
"integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz",
|
||||
"integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.60.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0",
|
||||
"@typescript-eslint/project-service": "8.60.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.60.1",
|
||||
"@typescript-eslint/types": "8.60.1",
|
||||
"@typescript-eslint/visitor-keys": "8.60.1",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -14779,16 +14779,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz",
|
||||
"integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz",
|
||||
"integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/typescript-estree": "8.60.0"
|
||||
"@typescript-eslint/scope-manager": "8.60.1",
|
||||
"@typescript-eslint/types": "8.60.1",
|
||||
"@typescript-eslint/typescript-estree": "8.60.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -14803,13 +14803,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz",
|
||||
"integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==",
|
||||
"version": "8.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz",
|
||||
"integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.1",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -18865,9 +18865,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.0.tgz",
|
||||
"integrity": "sha512-DRrk10z3sVPpguNe8od2cGNqZGqbT15rwAnxD4dG3b78mdNNb/gJyr8T834Oj518WcBmTktrt4FhdwZn09ZWSg==",
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz",
|
||||
"integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -22390,9 +22390,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-typescript": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz",
|
||||
"integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==",
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.5.tgz",
|
||||
"integrity": "sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -37446,9 +37446,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/oxlint": {
|
||||
"version": "1.67.0",
|
||||
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.67.0.tgz",
|
||||
"integrity": "sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ==",
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.68.0.tgz",
|
||||
"integrity": "sha512-dXcbq+xsmLrMy6T8d0euf3IYUfLmjHIE11pOxiUSi5LHkFZaYPv568R6sEjcavVpUxoaQe66UBuK4HEi74NxpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -37461,25 +37461,25 @@
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oxlint/binding-android-arm-eabi": "1.67.0",
|
||||
"@oxlint/binding-android-arm64": "1.67.0",
|
||||
"@oxlint/binding-darwin-arm64": "1.67.0",
|
||||
"@oxlint/binding-darwin-x64": "1.67.0",
|
||||
"@oxlint/binding-freebsd-x64": "1.67.0",
|
||||
"@oxlint/binding-linux-arm-gnueabihf": "1.67.0",
|
||||
"@oxlint/binding-linux-arm-musleabihf": "1.67.0",
|
||||
"@oxlint/binding-linux-arm64-gnu": "1.67.0",
|
||||
"@oxlint/binding-linux-arm64-musl": "1.67.0",
|
||||
"@oxlint/binding-linux-ppc64-gnu": "1.67.0",
|
||||
"@oxlint/binding-linux-riscv64-gnu": "1.67.0",
|
||||
"@oxlint/binding-linux-riscv64-musl": "1.67.0",
|
||||
"@oxlint/binding-linux-s390x-gnu": "1.67.0",
|
||||
"@oxlint/binding-linux-x64-gnu": "1.67.0",
|
||||
"@oxlint/binding-linux-x64-musl": "1.67.0",
|
||||
"@oxlint/binding-openharmony-arm64": "1.67.0",
|
||||
"@oxlint/binding-win32-arm64-msvc": "1.67.0",
|
||||
"@oxlint/binding-win32-ia32-msvc": "1.67.0",
|
||||
"@oxlint/binding-win32-x64-msvc": "1.67.0"
|
||||
"@oxlint/binding-android-arm-eabi": "1.68.0",
|
||||
"@oxlint/binding-android-arm64": "1.68.0",
|
||||
"@oxlint/binding-darwin-arm64": "1.68.0",
|
||||
"@oxlint/binding-darwin-x64": "1.68.0",
|
||||
"@oxlint/binding-freebsd-x64": "1.68.0",
|
||||
"@oxlint/binding-linux-arm-gnueabihf": "1.68.0",
|
||||
"@oxlint/binding-linux-arm-musleabihf": "1.68.0",
|
||||
"@oxlint/binding-linux-arm64-gnu": "1.68.0",
|
||||
"@oxlint/binding-linux-arm64-musl": "1.68.0",
|
||||
"@oxlint/binding-linux-ppc64-gnu": "1.68.0",
|
||||
"@oxlint/binding-linux-riscv64-gnu": "1.68.0",
|
||||
"@oxlint/binding-linux-riscv64-musl": "1.68.0",
|
||||
"@oxlint/binding-linux-s390x-gnu": "1.68.0",
|
||||
"@oxlint/binding-linux-x64-gnu": "1.68.0",
|
||||
"@oxlint/binding-linux-x64-musl": "1.68.0",
|
||||
"@oxlint/binding-openharmony-arm64": "1.68.0",
|
||||
"@oxlint/binding-win32-arm64-msvc": "1.68.0",
|
||||
"@oxlint/binding-win32-ia32-msvc": "1.68.0",
|
||||
"@oxlint/binding-win32-x64-msvc": "1.68.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"oxlint-tsgolint": ">=0.22.1",
|
||||
@@ -49354,7 +49354,7 @@
|
||||
"react-js-cron": "^5.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
@@ -49610,7 +49610,7 @@
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.2.6"
|
||||
"react": "^19.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
@@ -49619,9 +49619,9 @@
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-chord/node_modules/react": {
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
|
||||
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
||||
@@ -306,7 +306,7 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||
"@typescript-eslint/parser": "^8.59.4",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-loader": "^10.1.1",
|
||||
@@ -315,7 +315,7 @@
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"baseline-browser-mapping": "^2.10.33",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^10.0.0",
|
||||
"concurrently": "^10.0.3",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"css-loader": "^7.1.4",
|
||||
@@ -323,7 +323,7 @@
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-cypress": "^3.6.0",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
|
||||
@@ -353,7 +353,7 @@
|
||||
"lightningcss": "^1.32.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.67.0",
|
||||
"oxlint": "^1.68.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.8.3",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
|
||||
@@ -18,22 +18,6 @@
|
||||
"types": "./lib/authentication/index.d.ts",
|
||||
"default": "./lib/authentication/index.js"
|
||||
},
|
||||
"./dashboard": {
|
||||
"types": "./lib/dashboard/index.d.ts",
|
||||
"default": "./lib/dashboard/index.js"
|
||||
},
|
||||
"./dataset": {
|
||||
"types": "./lib/dataset/index.d.ts",
|
||||
"default": "./lib/dataset/index.js"
|
||||
},
|
||||
"./explore": {
|
||||
"types": "./lib/explore/index.d.ts",
|
||||
"default": "./lib/explore/index.js"
|
||||
},
|
||||
"./navigation": {
|
||||
"types": "./lib/navigation/index.d.ts",
|
||||
"default": "./lib/navigation/index.js"
|
||||
},
|
||||
"./commands": {
|
||||
"types": "./lib/commands/index.d.ts",
|
||||
"default": "./lib/commands/index.js"
|
||||
|
||||
@@ -213,55 +213,18 @@ export declare interface Event<T> {
|
||||
(listener: (e: T) => any, thisArgs?: any): Disposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context handed to an extension's `activate` function.
|
||||
*
|
||||
* `context.subscriptions` is provided for extensions to push their
|
||||
* {@link Disposable}s into. The host provides the array but does not dispose
|
||||
* it (lifecycle management is deferred).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export function activate(context: ExtensionContext) {
|
||||
* context.subscriptions.push(
|
||||
* commands.registerCommand('my_ext.hello', () => {}),
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface ExtensionContext {
|
||||
/**
|
||||
* Disposables pushed by the extension. Provided for extensions to track
|
||||
* their own registrations; the host does not dispose them.
|
||||
*/
|
||||
subscriptions: { dispose(): void }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of an extension's entry module (its `./index`).
|
||||
*
|
||||
* Extensions are encouraged to export an `activate(context)` function so that
|
||||
* their registrations are tracked via `context.subscriptions` regardless of
|
||||
* whether they run synchronously or asynchronously. For backward compatibility,
|
||||
* a module may instead register its contributions as top-level side effects when
|
||||
* the module is evaluated.
|
||||
*/
|
||||
export interface ExtensionModule {
|
||||
/**
|
||||
* Called by the host once the extension module has loaded. May be async; the
|
||||
* host awaits it before considering the extension active.
|
||||
*/
|
||||
activate?(context: ExtensionContext): void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Superset extension with its metadata.
|
||||
* Extensions are modular components that can extend Superset's functionality
|
||||
* by registering commands, views, menus, and editors as module-level side effects.
|
||||
*/
|
||||
export interface Extension {
|
||||
/** List of other extensions that this extension depends on */
|
||||
dependencies: string[];
|
||||
/** Human-readable description of the extension */
|
||||
description: string;
|
||||
/** List of other extensions that this extension depends on */
|
||||
extensionDependencies: string[];
|
||||
/** Unique identifier for the extension */
|
||||
id: string;
|
||||
/** Human-readable name of the extension */
|
||||
|
||||
@@ -43,9 +43,6 @@ export type SqlLabLocation =
|
||||
| 'results'
|
||||
| 'queryHistory';
|
||||
|
||||
/** Valid locations within the app shell (persist across all routes). */
|
||||
export type AppLocation = 'chatbot';
|
||||
|
||||
/**
|
||||
* Nested structure for view contributions by scope and location.
|
||||
* @example
|
||||
@@ -58,7 +55,6 @@ export type AppLocation = 'chatbot';
|
||||
*/
|
||||
export interface ViewContributions {
|
||||
sqllab?: Partial<Record<SqlLabLocation, View[]>>;
|
||||
app?: Partial<Record<AppLocation, View[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,114 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Dashboard namespace for Superset extensions (P3).
|
||||
*
|
||||
* Exposes dashboard identity and filter state as a stable semantic API.
|
||||
* Extensions must not depend on the Redux dashboard slice structure directly.
|
||||
*/
|
||||
|
||||
import { Event } from '../common';
|
||||
|
||||
/**
|
||||
* A single native filter's current selected value(s).
|
||||
* The value type is intentionally kept as `unknown` because filter values
|
||||
* are heterogeneous (date ranges, string lists, numbers, etc.).
|
||||
*/
|
||||
export interface FilterValue {
|
||||
/** The filter's stable id. */
|
||||
filterId: string;
|
||||
/** Display label of the filter. */
|
||||
label: string;
|
||||
/** Currently applied value, or `null` when the filter is cleared. */
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary of a single chart on the active dashboard.
|
||||
*
|
||||
* Exposes the identity, viz type, datasource, and current visibility of a
|
||||
* chart so extensions can answer both "which charts are visible?" and
|
||||
* "find the chart named X" without additional lookups.
|
||||
*/
|
||||
export interface ChartSummary {
|
||||
/** Numeric chart (slice) id. */
|
||||
chartId: number;
|
||||
/** Display name of the chart. */
|
||||
chartName: string;
|
||||
/** Visualization type key (e.g. `'echarts_timeseries_bar'`). */
|
||||
vizType: string;
|
||||
/** Datasource id, or `null` when not resolvable. */
|
||||
datasourceId: number | null;
|
||||
/** Datasource name, or `null` when not resolvable. */
|
||||
datasourceName: string | null;
|
||||
/** Whether the chart is currently visible (e.g. on the active tab). */
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized dashboard context exposed to extensions on the Dashboard page.
|
||||
*/
|
||||
export interface DashboardContext {
|
||||
/** Numeric dashboard id. */
|
||||
dashboardId: number;
|
||||
/** Display title of the dashboard. */
|
||||
title: string;
|
||||
/**
|
||||
* Active native filter values keyed by filter id.
|
||||
* Only includes filters that have a value applied.
|
||||
*/
|
||||
filters: FilterValue[];
|
||||
/**
|
||||
* Summaries of the dashboard's charts, including per-chart visibility.
|
||||
*
|
||||
* Optional: the contract is declared so extensions can compile against the
|
||||
* stable shape, but population is delivered in a later phase (see
|
||||
* CHATBOT_SIP.md §10/§11). The host returns an empty array until then.
|
||||
*/
|
||||
charts?: ChartSummary[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized dashboard context for the page currently being viewed,
|
||||
* or `undefined` when the user is not on a Dashboard page.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const dash = dashboard.getCurrentDashboard();
|
||||
* if (dash) {
|
||||
* console.log(dash.title, dash.filters);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export declare function getCurrentDashboard(): DashboardContext | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when the dashboard identity or its active filter values change.
|
||||
* Fired on native filter value changes and on navigation to a different dashboard.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sub = dashboard.onDidChangeDashboard(dash => {
|
||||
* chatbot.updateContext({ dashboard: dash });
|
||||
* });
|
||||
* sub.dispose();
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidChangeDashboard: Event<DashboardContext>;
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Dataset namespace for Superset extensions (P3).
|
||||
*
|
||||
* Exposes the dataset currently being viewed as a stable semantic API.
|
||||
* Aligned with backend-enforced dataset visibility and column-access semantics.
|
||||
*/
|
||||
|
||||
import { Event } from '../common';
|
||||
|
||||
/**
|
||||
* Normalized dataset context exposed to extensions on the Dataset page.
|
||||
*/
|
||||
export interface DatasetContext {
|
||||
/** Numeric dataset id. */
|
||||
datasetId: number;
|
||||
/** Display name (table name or virtual dataset name). */
|
||||
datasetName: string;
|
||||
/** Schema the dataset belongs to, if applicable. */
|
||||
schema: string | null;
|
||||
/** Catalog the dataset belongs to, if applicable. */
|
||||
catalog: string | null;
|
||||
/** Database name backing this dataset. */
|
||||
databaseName: string | null;
|
||||
/** Whether this is a virtual (SQL-defined) dataset. */
|
||||
isVirtual: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized dataset context for the page currently being viewed,
|
||||
* or `undefined` when the user is not on a Dataset page.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const ds = dataset.getCurrentDataset();
|
||||
* if (ds) {
|
||||
* console.log(ds.datasetName, ds.schema);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export declare function getCurrentDataset(): DatasetContext | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when the focused dataset changes (e.g. the user navigates to a
|
||||
* different dataset detail page).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sub = dataset.onDidChangeDataset(ds => {
|
||||
* chatbot.updateContext({ dataset: ds });
|
||||
* });
|
||||
* sub.dispose();
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidChangeDataset: Event<DatasetContext>;
|
||||
@@ -1,75 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Explore namespace for Superset extensions (P3).
|
||||
*
|
||||
* Exposes the current chart/explore context as a stable semantic API.
|
||||
* Normalized over Explore Redux state — extensions must not depend on
|
||||
* the Redux slice structure directly.
|
||||
*/
|
||||
|
||||
import { Event } from '../common';
|
||||
|
||||
/**
|
||||
* Normalized chart context exposed to extensions during an Explore session.
|
||||
* Covers saved chart identity and transient editing context; excludes raw
|
||||
* form-data internals and datasource-implementation details.
|
||||
*/
|
||||
export interface ChartContext {
|
||||
/** The saved chart id, or `null` when the chart has not been persisted. */
|
||||
chartId: number | null;
|
||||
/** Display name of the saved chart, or `null` for a new/unsaved chart. */
|
||||
chartName: string | null;
|
||||
/** The visualization type currently selected in the editor. */
|
||||
vizType: string;
|
||||
/** Id of the datasource backing the chart (physical or virtual dataset). */
|
||||
datasourceId: number | null;
|
||||
/** Human-readable datasource name. */
|
||||
datasourceName: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized chart context for the active Explore session, or
|
||||
* `undefined` when the user is not on the Explore page.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const chart = explore.getCurrentChart();
|
||||
* if (chart) {
|
||||
* console.log(chart.vizType, chart.chartName);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export declare function getCurrentChart(): ChartContext | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when the chart context changes within the active Explore session
|
||||
* (e.g. when the viz type, datasource, or saved name changes).
|
||||
* Not fired during route changes — subscribe to `navigation.onDidChangePage` for those.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sub = explore.onDidChangeChart(chart => {
|
||||
* chatbot.updateContext({ chart });
|
||||
* });
|
||||
* sub.dispose();
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidChangeChart: Event<ChartContext>;
|
||||
@@ -19,13 +19,9 @@
|
||||
export * as common from './common';
|
||||
export * as authentication from './authentication';
|
||||
export * as commands from './commands';
|
||||
export * as dashboard from './dashboard';
|
||||
export * as dataset from './dataset';
|
||||
export * as editors from './editors';
|
||||
export * as explore from './explore';
|
||||
export * as extensions from './extensions';
|
||||
export * as menus from './menus';
|
||||
export * as navigation from './navigation';
|
||||
export * as sqlLab from './sqlLab';
|
||||
export * as views from './views';
|
||||
export * as contributions from './contributions';
|
||||
|
||||
@@ -1,84 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Navigation namespace for Superset extensions (P3).
|
||||
*
|
||||
* Exposes the current application surface so extensions can react to route
|
||||
* changes without polling. Entity-level context (chart, dashboard, dataset)
|
||||
* is intentionally not included here — use the surface-specific namespace
|
||||
* (`explore`, `dashboard`, `dataset`) to retrieve entity payloads.
|
||||
*/
|
||||
|
||||
import { Event } from '../common';
|
||||
|
||||
/**
|
||||
* The set of top-level application surfaces.
|
||||
*
|
||||
* `'explore'`, `'dashboard'` and `'dataset'` are the single-entity
|
||||
* editing/viewing surfaces where `explore.getCurrentChart()` /
|
||||
* `dashboard.getCurrentDashboard()` / `dataset.getCurrentDataset()` resolve to a
|
||||
* concrete entity. `'chart_list'`, `'dashboard_list'` and `'dataset_list'` are
|
||||
* the browse/list surfaces, distinct from those because no single entity is
|
||||
* active. `'sqllab'` is the SQL editor where `sqlLab.getCurrentTab()` resolves;
|
||||
* `'query_history'` and `'saved_queries'` are the related SQL Lab browse pages,
|
||||
* which are not the editor. `'other'` covers any route not explicitly enumerated.
|
||||
*/
|
||||
export type PageType =
|
||||
| 'dashboard'
|
||||
| 'dashboard_list'
|
||||
| 'explore'
|
||||
| 'chart_list'
|
||||
| 'sqllab'
|
||||
| 'query_history'
|
||||
| 'saved_queries'
|
||||
| 'dataset'
|
||||
| 'dataset_list'
|
||||
| 'home'
|
||||
| 'other';
|
||||
|
||||
/**
|
||||
* Returns the current page surface type.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const pageType = navigation.getPageType();
|
||||
* if (pageType === 'dashboard') {
|
||||
* const ctx = dashboard.getCurrentDashboard();
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export declare function getPageType(): PageType;
|
||||
|
||||
/**
|
||||
* Event fired whenever the user navigates to a different surface.
|
||||
* Use the surface-specific namespace to read entity context after the event.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sub = navigation.onDidChangePage(pageType => {
|
||||
* if (pageType === 'dashboard') {
|
||||
* const ctx = dashboard.getCurrentDashboard();
|
||||
* }
|
||||
* });
|
||||
* // later:
|
||||
* sub.dispose();
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidChangePage: Event<PageType>;
|
||||
@@ -48,12 +48,6 @@ export interface View {
|
||||
name: string;
|
||||
/** Optional description of the view, for display in contribution manifests. */
|
||||
description?: string;
|
||||
/**
|
||||
* Optional icon identifier for the view, used in admin pickers and manifest
|
||||
* listings. Static — set once at registerView() time.
|
||||
* Dynamic icon states (e.g. notification badge) are the extension's concern.
|
||||
*/
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,12 +56,12 @@ export interface View {
|
||||
* The view provider function is called when the UI renders the location,
|
||||
* and should return a React element to display.
|
||||
*
|
||||
* @param view The view descriptor (id, name, and optional icon/description).
|
||||
* @param view The view descriptor (id and name).
|
||||
* @param location The location where this view should appear (e.g. "sqllab.panels").
|
||||
* @param provider A function that returns the React element to render.
|
||||
* @returns A Disposable that unregisters the view when disposed.
|
||||
*
|
||||
* @example SQL Lab panel
|
||||
* @example
|
||||
* ```typescript
|
||||
* views.registerView(
|
||||
* { id: 'my_ext.result_stats', name: 'Result Stats' },
|
||||
@@ -75,15 +69,6 @@ export interface View {
|
||||
* () => <ResultStatsPanel />,
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @example Chatbot bubble (`core.chatbot` — singleton, host renders one)
|
||||
* ```typescript
|
||||
* views.registerView(
|
||||
* { id: 'my_ext.chatbot', name: 'My Chatbot', icon: 'Bubble' },
|
||||
* 'core.chatbot',
|
||||
* () => <ChatbotApp />,
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export declare function registerView(
|
||||
view: View,
|
||||
|
||||
@@ -118,7 +118,6 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
||||
description: t(`Select dimension and values`),
|
||||
default: { dimension: '', values: [] },
|
||||
validators: [], // No validation - rely on visibility
|
||||
renderTrigger: true,
|
||||
tabOverride: 'matrixify',
|
||||
shouldMapStateToProps: (prevState, state) => {
|
||||
// Recalculate when any relevant form_data field changes
|
||||
|
||||
@@ -17,8 +17,20 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax -- whole React import is required for `reactify.test.tsx` Jest test passing.
|
||||
import { Component, ComponentClass, WeakValidationMap } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
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
|
||||
// leading to (likely) unexpected behaviors. We should either require Props to not
|
||||
@@ -49,66 +61,103 @@ export interface RenderFuncType<Props> {
|
||||
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>(
|
||||
renderFn: RenderFuncType<Props>,
|
||||
callbacks?: LifeCycleCallbacks,
|
||||
): ComponentClass<Props & ReactifyProps> {
|
||||
class ReactifiedComponent extends Component<Props & ReactifyProps> {
|
||||
container?: HTMLDivElement;
|
||||
): ComponentType<Props & ReactifyProps> {
|
||||
const ReactifiedComponent = forwardRef<
|
||||
ReactifiedComponentRef,
|
||||
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;
|
||||
});
|
||||
|
||||
constructor(props: Props & ReactifyProps) {
|
||||
super(props);
|
||||
this.setContainerRef = this.setContainerRef.bind(this);
|
||||
}
|
||||
// Expose container via ref for external access
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
get container() {
|
||||
return containerRef.current ?? undefined;
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
componentDidMount() {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.container = undefined;
|
||||
if (callbacks?.componentWillUnmount) {
|
||||
callbacks.componentWillUnmount.bind(this)();
|
||||
// Execute renderFn on mount and every update (mimics componentDidMount + componentDidUpdate)
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
// `forwardRef` widens the props parameter to `PropsWithoutRef<...>`,
|
||||
// which TypeScript can't narrow back to `Props & ReactifyProps` when
|
||||
// `Props` is a generic `object`. The values are identical at runtime,
|
||||
// so assert the original prop shape for `renderFn`.
|
||||
renderFn(
|
||||
containerRef.current,
|
||||
props as Readonly<Props & ReactifyProps>,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setContainerRef(ref: HTMLDivElement) {
|
||||
this.container = ref;
|
||||
}
|
||||
// Cleanup on unmount
|
||||
useEffect(
|
||||
() => () => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
execute() {
|
||||
if (this.container) {
|
||||
renderFn(this.container, this.props);
|
||||
}
|
||||
}
|
||||
const { id, className } = props;
|
||||
|
||||
render() {
|
||||
const { id, className } = this.props;
|
||||
|
||||
return <div ref={this.setContainerRef} id={id} className={className} />;
|
||||
}
|
||||
}
|
||||
|
||||
const ReactifiedClass: ComponentClass<Props & ReactifyProps> =
|
||||
ReactifiedComponent;
|
||||
return <div ref={containerRef} id={id} className={className} />;
|
||||
});
|
||||
|
||||
if (renderFn.displayName) {
|
||||
ReactifiedClass.displayName = renderFn.displayName;
|
||||
ReactifiedComponent.displayName = renderFn.displayName;
|
||||
}
|
||||
// eslint-disable-next-line react/forbid-foreign-prop-types
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- forwardRef static field types don't line up with renderFn's validator types
|
||||
const result = ReactifiedComponent as any;
|
||||
|
||||
if (renderFn.propTypes) {
|
||||
ReactifiedClass.propTypes = {
|
||||
...ReactifiedClass.propTypes,
|
||||
result.propTypes = {
|
||||
...result.propTypes,
|
||||
...renderFn.propTypes,
|
||||
};
|
||||
}
|
||||
|
||||
if (renderFn.defaultProps) {
|
||||
ReactifiedClass.defaultProps = renderFn.defaultProps;
|
||||
result.defaultProps = renderFn.defaultProps;
|
||||
}
|
||||
|
||||
return ReactifiedComponent;
|
||||
return result as unknown as ComponentType<Props & ReactifyProps>;
|
||||
}
|
||||
|
||||
@@ -519,8 +519,7 @@ const Select = forwardRef(
|
||||
handleSelectAll();
|
||||
}}
|
||||
>
|
||||
{t('Select all')}{' '}
|
||||
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
|
||||
{t('Select all')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -537,8 +536,7 @@ const Select = forwardRef(
|
||||
handleDeselectAll();
|
||||
}}
|
||||
>
|
||||
{t('Clear')}{' '}
|
||||
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
|
||||
{t('Clear')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
|
||||
</Button>
|
||||
</StyledBulkActionsContainer>
|
||||
),
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { reactify } from '@superset-ui/core';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { RenderFuncType } from '../../../src/chart/components/reactify';
|
||||
|
||||
describe('reactify(renderFn)', () => {
|
||||
@@ -52,48 +52,41 @@ describe('reactify(renderFn)', () => {
|
||||
componentWillUnmount: willUnmountCb,
|
||||
});
|
||||
|
||||
class TestComponent extends PureComponent<{}, { content: string }> {
|
||||
constructor(props = {}) {
|
||||
super(props);
|
||||
this.state = { content: 'abc' };
|
||||
}
|
||||
function TestComponent() {
|
||||
const [content, setContent] = useState('abc');
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
this.setState({ content: 'def' });
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setContent('def');
|
||||
}, 10);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
const { content } = this.state;
|
||||
|
||||
return <TheChart id="test" content={content} />;
|
||||
}
|
||||
return <TheChart id="test" content={content} />;
|
||||
}
|
||||
|
||||
class AnotherTestComponent extends PureComponent<{}, {}> {
|
||||
render() {
|
||||
return <TheChartWithWillUnmountHook id="another_test" />;
|
||||
}
|
||||
function AnotherTestComponent() {
|
||||
return <TheChartWithWillUnmountHook id="another_test" />;
|
||||
}
|
||||
|
||||
test('returns a React component class', () =>
|
||||
new Promise(done => {
|
||||
render(<TestComponent />);
|
||||
beforeEach(() => {
|
||||
(renderFn as jest.Mock).mockClear();
|
||||
willUnmountCb.mockClear();
|
||||
});
|
||||
|
||||
expect(renderFn).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText('abc')).toBeInTheDocument();
|
||||
expect(screen.getByText('abc').parentNode).toHaveAttribute('id', 'test');
|
||||
setTimeout(() => {
|
||||
expect(renderFn).toHaveBeenCalledTimes(2);
|
||||
expect(screen.getByText('def')).toBeInTheDocument();
|
||||
expect(screen.getByText('def').parentNode).toHaveAttribute(
|
||||
'id',
|
||||
'test',
|
||||
);
|
||||
done(undefined);
|
||||
}, 20);
|
||||
}));
|
||||
test('returns a React component and re-renders on prop changes', async () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(renderFn).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText('abc')).toBeInTheDocument();
|
||||
expect(screen.getByText('abc').parentNode).toHaveAttribute('id', 'test');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('def')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('def').parentNode).toHaveAttribute('id', 'test');
|
||||
expect(renderFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
describe('displayName', () => {
|
||||
test('has displayName if renderFn.displayName is defined', () => {
|
||||
expect(TheChart.displayName).toEqual('BoldText');
|
||||
@@ -126,20 +119,16 @@ describe('reactify(renderFn)', () => {
|
||||
expect(AnotherChart.defaultProps).toBeUndefined();
|
||||
});
|
||||
});
|
||||
test('does not try to render if not mounted', () => {
|
||||
test('calls renderFn when container is set', () => {
|
||||
const anotherRenderFn = jest.fn();
|
||||
const AnotherChart = reactify(anotherRenderFn); // enables valid new AnotherChart() call
|
||||
// @ts-expect-error
|
||||
new AnotherChart({ id: 'test' }).execute();
|
||||
expect(anotherRenderFn).not.toHaveBeenCalled();
|
||||
const AnotherChart = reactify(anotherRenderFn);
|
||||
const { unmount } = render(<AnotherChart id="test" />);
|
||||
expect(anotherRenderFn).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
test('calls willUnmount hook when it is provided', () => {
|
||||
const { unmount } = render(<AnotherTestComponent />);
|
||||
unmount();
|
||||
expect(willUnmountCb).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
test('calls willUnmount hook when it is provided', () =>
|
||||
new Promise(done => {
|
||||
const { unmount } = render(<AnotherTestComponent />);
|
||||
setTimeout(() => {
|
||||
unmount();
|
||||
expect(willUnmountCb).toHaveBeenCalledTimes(1);
|
||||
done(undefined);
|
||||
}, 20);
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -182,7 +182,10 @@ testWithAssets(
|
||||
// Now track POST /api/v1/chart/data requests around Clear All
|
||||
const postsAfterClearAll: string[] = [];
|
||||
const handler = (req: any) => {
|
||||
if (req.url().includes('/api/v1/chart/data') && req.method() === 'POST') {
|
||||
if (
|
||||
req.url().includes('/api/v1/chart/data') &&
|
||||
req.method() === 'POST'
|
||||
) {
|
||||
postsAfterClearAll.push(req.url());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.2.6"
|
||||
"react": "^19.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@math.gl/web-mercator": "^4.1.0",
|
||||
"mapbox-gl": "^3.24.0",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"react-map-gl": "^8.1.0",
|
||||
"react-map-gl": "^8.1.1",
|
||||
"supercluster": "^8.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,125 +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 { render, screen } from 'spec/helpers/testing-library';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { views } from 'src/core';
|
||||
import { loadExtensionSettings } from 'src/core/extensions';
|
||||
import { CHATBOT_LOCATION } from 'src/views/contributions';
|
||||
import ChatbotMount from '.';
|
||||
|
||||
const disposables: Array<{ dispose: () => void }> = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
// The settings store is a module singleton; reset it to the empty default
|
||||
// (no admin pin) before each test by loading from a mocked API response.
|
||||
jest.spyOn(SupersetClient, 'get').mockResolvedValue({
|
||||
json: { result: { active_chatbot_id: null } },
|
||||
} as any);
|
||||
await loadExtensionSettings();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
disposables.forEach(d => d.dispose());
|
||||
disposables.length = 0;
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('renders nothing when no chatbot extension is registered', async () => {
|
||||
render(<ChatbotMount />);
|
||||
|
||||
// Wait a tick for the settings load to resolve; the corner must stay empty
|
||||
// even after the gate opens (no chatbot registered → nothing to render).
|
||||
await Promise.resolve();
|
||||
expect(screen.queryByTestId('chatbot-mount')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the registered chatbot inside the fixed mount slot', async () => {
|
||||
const provider = () => <div>My Chatbot Bubble</div>;
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'core.chatbot', name: 'Superset Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
provider,
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChatbotMount />);
|
||||
|
||||
// findBy* awaits the re-render after the initial settings load resolves.
|
||||
expect(await screen.findByTestId('chatbot-mount')).toBeInTheDocument();
|
||||
expect(screen.getByText('My Chatbot Bubble')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders only the first-to-register chatbot when several are installed', async () => {
|
||||
const firstProvider = () => <div>First Bubble</div>;
|
||||
const secondProvider = () => <div>Second Bubble</div>;
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'first.chatbot', name: 'First Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
firstProvider,
|
||||
),
|
||||
views.registerView(
|
||||
{ id: 'second.chatbot', name: 'Second Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
secondProvider,
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChatbotMount />);
|
||||
|
||||
expect(await screen.findByText('First Bubble')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Second Bubble')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('isolates a failing chatbot so it does not crash the host', async () => {
|
||||
const FailingChatbot = () => {
|
||||
throw new Error('chatbot blew up');
|
||||
};
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'core.chatbot', name: 'Superset Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
() => <FailingChatbot />,
|
||||
),
|
||||
);
|
||||
|
||||
// The host-owned error boundary catches the failure; render does not throw.
|
||||
expect(() => render(<ChatbotMount />)).not.toThrow();
|
||||
// The mount slot still renders post-gate (the boundary lives inside it);
|
||||
// awaiting it confirms the provider was actually exercised and contained.
|
||||
expect(await screen.findByTestId('chatbot-mount')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('isolates a chatbot whose provider function itself throws', async () => {
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'core.chatbot', name: 'Superset Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
() => {
|
||||
throw new Error('provider blew up');
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// ChatbotRenderer wraps provider() in a component so ErrorBoundary catches
|
||||
// synchronous throws from the provider function, not just from its output.
|
||||
expect(() => render(<ChatbotMount />)).not.toThrow();
|
||||
expect(await screen.findByTestId('chatbot-mount')).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,124 +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 ReactElement,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { logging } from '@apache-superset/core/utils';
|
||||
import { css, useTheme } from '@apache-superset/core/theme';
|
||||
import { ErrorBoundary } from 'src/components/ErrorBoundary';
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
import { store } from 'src/views/store';
|
||||
import { getActiveChatbot } from 'src/core/chatbot';
|
||||
import { subscribeToRegistry, getRegistryVersion } from 'src/core/views';
|
||||
import {
|
||||
getExtensionSettingsSnapshot,
|
||||
loadExtensionSettings,
|
||||
subscribeToExtensionSettings,
|
||||
} from 'src/core/extensions';
|
||||
|
||||
const CHATBOT_EDGE_MARGIN = 24;
|
||||
|
||||
/**
|
||||
* Wraps the chatbot provider in a React component so that ErrorBoundary can
|
||||
* catch synchronous throws from the provider function itself. Calling
|
||||
* `provider()` inline (e.g. `{activeChatbot.provider()}`) would throw outside
|
||||
* React's render boundary and crash the host.
|
||||
*/
|
||||
const ChatbotRenderer = ({ provider }: { provider: () => ReactElement }) =>
|
||||
provider();
|
||||
|
||||
const ChatbotMount = () => {
|
||||
const theme = useTheme();
|
||||
// Notify once per mount; a crash can re-render and would otherwise re-toast.
|
||||
const crashNotified = useRef(false);
|
||||
// Defer chatbot resolution until the first settings load resolves. Otherwise
|
||||
// the initial empty-default snapshot (no pin) would briefly resolve the
|
||||
// first-registered chatbot even when the DB pins a different one, mounting
|
||||
// the wrong provider until the async settings response arrives.
|
||||
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
||||
|
||||
// The active chatbot is a function of two host-owned stores: the admin
|
||||
// settings (active chatbot id) and the view registry (which chatbots are
|
||||
// registered). Both are read via useSyncExternalStore so this re-resolves
|
||||
// when either changes — no local copy of the settings state.
|
||||
const settings = useSyncExternalStore(
|
||||
subscribeToExtensionSettings,
|
||||
getExtensionSettingsSnapshot,
|
||||
);
|
||||
const registryVersion = useSyncExternalStore(
|
||||
subscribeToRegistry,
|
||||
getRegistryVersion,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Settings fetch failure is non-fatal: the store keeps its empty default,
|
||||
// which getActiveChatbot treats as "no admin pin" (falls back to the
|
||||
// first-registered chatbot). Either way, unblock rendering once the request
|
||||
// settles so a failed fetch never permanently hides the chatbot.
|
||||
loadExtensionSettings()
|
||||
.catch(() => {})
|
||||
.finally(() => setSettingsLoaded(true));
|
||||
}, []);
|
||||
|
||||
const activeChatbot = useMemo(
|
||||
() => getActiveChatbot(settings.active_chatbot_id),
|
||||
[settings, registryVersion],
|
||||
);
|
||||
|
||||
if (!settingsLoaded || !activeChatbot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test="chatbot-mount"
|
||||
css={css`
|
||||
position: fixed;
|
||||
right: ${CHATBOT_EDGE_MARGIN}px;
|
||||
bottom: ${CHATBOT_EDGE_MARGIN}px;
|
||||
/* Above dashboard content and the toast layer, below modal dialogs. */
|
||||
z-index: ${theme.zIndexPopupBase + 2};
|
||||
`}
|
||||
>
|
||||
<ErrorBoundary
|
||||
showMessage={false}
|
||||
onError={(error: Error) => {
|
||||
// Fault isolation (SIP §4.5): contain the crash, log it, surface a
|
||||
// one-time notification, and leave the corner empty rather than
|
||||
// parking a persistent error card.
|
||||
logging.error('[chatbot] provider crashed', error);
|
||||
if (!crashNotified.current) {
|
||||
crashNotified.current = true;
|
||||
store.dispatch(addDangerToast(t('The chatbot failed to load.')));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChatbotRenderer provider={activeChatbot.provider} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatbotMount;
|
||||
@@ -1,132 +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 React from 'react';
|
||||
import { views } from 'src/core/views';
|
||||
import { CHATBOT_LOCATION } from 'src/views/contributions';
|
||||
import { getActiveChatbot } from './index';
|
||||
|
||||
const disposables: Array<{ dispose: () => void }> = [];
|
||||
|
||||
afterEach(() => {
|
||||
disposables.forEach(d => d.dispose());
|
||||
disposables.length = 0;
|
||||
});
|
||||
|
||||
test('getActiveChatbot returns undefined when no chatbot is registered', () => {
|
||||
expect(getActiveChatbot()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getActiveChatbot resolves the single registered chatbot', () => {
|
||||
const provider = () => React.createElement('div', null, 'Chatbot');
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'core.chatbot', name: 'Superset Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
provider,
|
||||
),
|
||||
);
|
||||
|
||||
const active = getActiveChatbot();
|
||||
expect(active).toEqual({ id: 'core.chatbot', provider });
|
||||
});
|
||||
|
||||
test('getActiveChatbot picks the first-to-register when multiple are installed', () => {
|
||||
const firstProvider = () => React.createElement('div', null, 'First');
|
||||
const secondProvider = () => React.createElement('div', null, 'Second');
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'first.chatbot', name: 'First Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
firstProvider,
|
||||
),
|
||||
views.registerView(
|
||||
{ id: 'second.chatbot', name: 'Second Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
secondProvider,
|
||||
),
|
||||
);
|
||||
|
||||
const active = getActiveChatbot();
|
||||
expect(active?.id).toBe('first.chatbot');
|
||||
expect(active?.provider).toBe(firstProvider);
|
||||
});
|
||||
|
||||
test('getActiveChatbot ignores views registered at other locations', () => {
|
||||
const provider = () => React.createElement('div', null, 'Panel');
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'some.panel', name: 'Some Panel' },
|
||||
'sqllab.panels',
|
||||
provider,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getActiveChatbot()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getActiveChatbot stops resolving a chatbot once it is disposed', () => {
|
||||
const provider = () => React.createElement('div', null, 'Chatbot');
|
||||
const disposable = views.registerView(
|
||||
{ id: 'core.chatbot', name: 'Superset Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
provider,
|
||||
);
|
||||
|
||||
expect(getActiveChatbot()?.id).toBe('core.chatbot');
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(getActiveChatbot()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getActiveChatbot honours the admin-pinned selection', () => {
|
||||
const firstProvider = () => React.createElement('div', null, 'First');
|
||||
const secondProvider = () => React.createElement('div', null, 'Second');
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'first.chatbot', name: 'First Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
firstProvider,
|
||||
),
|
||||
views.registerView(
|
||||
{ id: 'second.chatbot', name: 'Second Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
secondProvider,
|
||||
),
|
||||
);
|
||||
|
||||
const active = getActiveChatbot('second.chatbot');
|
||||
expect(active?.id).toBe('second.chatbot');
|
||||
expect(active?.provider).toBe(secondProvider);
|
||||
});
|
||||
|
||||
test('getActiveChatbot falls back to first-registered when pinned id is unknown', () => {
|
||||
const provider = () => React.createElement('div', null, 'First');
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'first.chatbot', name: 'First Chatbot' },
|
||||
CHATBOT_LOCATION,
|
||||
provider,
|
||||
),
|
||||
);
|
||||
|
||||
// 'stale.chatbot' was once the admin pin but is no longer registered.
|
||||
const active = getActiveChatbot('stale.chatbot');
|
||||
expect(active?.id).toBe('first.chatbot');
|
||||
});
|
||||
@@ -1,79 +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.
|
||||
*/
|
||||
/**
|
||||
* @fileoverview Host-internal resolver for the exclusive `core.chatbot`
|
||||
* contribution area.
|
||||
*
|
||||
* `core.chatbot` is a singleton contribution area: multiple chatbot
|
||||
* extensions may register a view there, but the host renders exactly one.
|
||||
* This module owns the host-side selection policy.
|
||||
*
|
||||
* This is host-internal infrastructure — it is NOT part of the public
|
||||
* `@apache-superset/core` API. Extensions register via the public
|
||||
* `views.registerView()`; only the host resolves which one is active.
|
||||
*/
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { CHATBOT_LOCATION } from 'src/views/contributions';
|
||||
import { getRegisteredViewIds, getViewProvider } from 'src/core/views';
|
||||
|
||||
/**
|
||||
* The resolved active chatbot: a view id paired with its renderable provider.
|
||||
*/
|
||||
export interface ActiveChatbot {
|
||||
/** The registered view id of the selected chatbot. */
|
||||
id: string;
|
||||
/** The provider that renders the chatbot's React element. */
|
||||
provider: () => ReactElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves which single chatbot extension is currently active.
|
||||
*
|
||||
* Selection policy:
|
||||
* - If no chatbot is registered, returns `undefined` — the corner stays empty.
|
||||
* - If `adminSelectedId` matches a registered chatbot, that one wins.
|
||||
* - Otherwise the first-registered chatbot is used as a fallback.
|
||||
* The active chatbot pin is set only via the backend DB; when no pin is set
|
||||
* (active_chatbot_id is null), the fallback is the first-registered chatbot.
|
||||
*
|
||||
* @param adminSelectedId The id stored in the DB "Default chatbot" setting, if any.
|
||||
* @returns The active chatbot's id and provider, or `undefined` if none.
|
||||
*/
|
||||
export const getActiveChatbot = (
|
||||
adminSelectedId?: string | null,
|
||||
): ActiveChatbot | undefined => {
|
||||
const registeredIds = getRegisteredViewIds(CHATBOT_LOCATION);
|
||||
if (registeredIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// When the DB pin names a registered candidate, use it; otherwise fall back
|
||||
// to the first registered chatbot in registration order.
|
||||
// `getRegisteredViewIds` and `getViewProvider` read the same synchronous
|
||||
// registry maps, so a candidate id always has a live provider; the final
|
||||
// guard is cheap defensiveness, not a fallback path.
|
||||
const selectedId =
|
||||
adminSelectedId && registeredIds.includes(adminSelectedId)
|
||||
? adminSelectedId
|
||||
: registeredIds[0];
|
||||
|
||||
const provider = getViewProvider(CHATBOT_LOCATION, selectedId);
|
||||
return provider ? { id: selectedId, provider } : undefined;
|
||||
};
|
||||
@@ -1,220 +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.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Captured listeners — allows tests to trigger action notifications manually.
|
||||
// ---------------------------------------------------------------------------
|
||||
type ListenerEntry = {
|
||||
predicate: (action: { type: string }) => boolean;
|
||||
effect: (action: { type: string }) => void;
|
||||
};
|
||||
|
||||
const capturedListeners: ListenerEntry[] = [];
|
||||
|
||||
// Declared before jest.mock so the factory closure can reference it.
|
||||
let mockState: Record<string, unknown>;
|
||||
|
||||
jest.mock('src/views/store', () => ({
|
||||
store: { getState: () => mockState, dispatch: jest.fn() },
|
||||
listenerMiddleware: {
|
||||
startListening: (opts: {
|
||||
predicate: (action: { type: string }) => boolean;
|
||||
effect: (action: { type: string }) => void;
|
||||
}) => {
|
||||
const entry = { predicate: opts.predicate, effect: opts.effect };
|
||||
capturedListeners.push(entry);
|
||||
return () => {
|
||||
const idx = capturedListeners.indexOf(entry);
|
||||
if (idx !== -1) capturedListeners.splice(idx, 1);
|
||||
};
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../navigation', () => ({
|
||||
navigation: { getPageType: jest.fn(() => 'dashboard') },
|
||||
}));
|
||||
|
||||
function dispatch(actionType: string) {
|
||||
const action = { type: actionType };
|
||||
capturedListeners
|
||||
.filter(e => e.predicate(action))
|
||||
.forEach(e => e.effect(action));
|
||||
}
|
||||
|
||||
// Imported after mocks
|
||||
// eslint-disable-next-line import/first
|
||||
import { dashboard } from './index';
|
||||
|
||||
function makeState(
|
||||
overrides: Partial<{
|
||||
dashboardInfo: unknown;
|
||||
nativeFilters: unknown;
|
||||
dataMask: unknown;
|
||||
sliceEntities: unknown;
|
||||
dashboardLayout: unknown;
|
||||
}> = {},
|
||||
) {
|
||||
return {
|
||||
dashboardInfo: { id: 1, dashboard_title: 'Sales', slug: 'sales' },
|
||||
nativeFilters: { filters: { 'filter-1': { name: 'Region' } } },
|
||||
dataMask: { 'filter-1': { filterState: { value: ['West'] } } },
|
||||
sliceEntities: { slices: {} },
|
||||
dashboardLayout: { present: {} },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockState = makeState();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
capturedListeners.length = 0;
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('getCurrentDashboard returns undefined when not on dashboard page', () => {
|
||||
const { navigation } = jest.requireMock('../navigation');
|
||||
(navigation.getPageType as jest.Mock).mockReturnValueOnce('explore');
|
||||
expect(dashboard.getCurrentDashboard()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getCurrentDashboard returns undefined when dashboardInfo is absent', () => {
|
||||
mockState = makeState({ dashboardInfo: undefined });
|
||||
expect(dashboard.getCurrentDashboard()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getCurrentDashboard returns dashboard context with active filters', () => {
|
||||
expect(dashboard.getCurrentDashboard()).toEqual({
|
||||
dashboardId: 1,
|
||||
title: 'Sales',
|
||||
filters: [{ filterId: 'filter-1', label: 'Region', value: ['West'] }],
|
||||
// No charts on the (empty) layout fixture.
|
||||
charts: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('getCurrentDashboard reports charts placed on the dashboard layout', () => {
|
||||
mockState = makeState({
|
||||
sliceEntities: {
|
||||
slices: {
|
||||
42: {
|
||||
slice_name: 'Revenue by Region',
|
||||
viz_type: 'echarts_timeseries_bar',
|
||||
datasource_id: 7,
|
||||
datasource_name: 'cleaned_sales',
|
||||
},
|
||||
},
|
||||
},
|
||||
dashboardLayout: {
|
||||
present: {
|
||||
'CHART-abc': { id: 'CHART-abc', type: 'CHART', meta: { chartId: 42 } },
|
||||
// A chart id with no matching slice entity still appears, with blanks.
|
||||
'CHART-def': { id: 'CHART-def', type: 'CHART', meta: { chartId: 99 } },
|
||||
// Non-chart components are ignored.
|
||||
'TAB-xyz': { id: 'TAB-xyz', type: 'TAB', meta: {} },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(dashboard.getCurrentDashboard()?.charts).toEqual([
|
||||
{
|
||||
chartId: 42,
|
||||
chartName: 'Revenue by Region',
|
||||
vizType: 'echarts_timeseries_bar',
|
||||
datasourceId: 7,
|
||||
datasourceName: 'cleaned_sales',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
chartId: 99,
|
||||
chartName: '',
|
||||
vizType: '',
|
||||
datasourceId: null,
|
||||
datasourceName: null,
|
||||
isVisible: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('getCurrentDashboard excludes filters with null value', () => {
|
||||
mockState = makeState({
|
||||
dataMask: { 'filter-1': { filterState: { value: null } } },
|
||||
});
|
||||
expect(dashboard.getCurrentDashboard()?.filters).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('getCurrentDashboard excludes dataMask entries not in nativeFilters', () => {
|
||||
mockState = makeState({
|
||||
dataMask: { 'chart-filter': { filterState: { value: 'foo' } } },
|
||||
});
|
||||
expect(dashboard.getCurrentDashboard()?.filters).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('filter array value is a defensive copy — mutation does not affect Redux state', () => {
|
||||
const ctx = dashboard.getCurrentDashboard();
|
||||
const original = [
|
||||
...(mockState as any).dataMask['filter-1'].filterState.value,
|
||||
];
|
||||
(ctx!.filters[0].value as string[]).push('East');
|
||||
expect((mockState as any).dataMask['filter-1'].filterState.value).toEqual(
|
||||
original,
|
||||
);
|
||||
});
|
||||
|
||||
// Action type strings match the constants in src/dashboard/actions/hydrate
|
||||
// and src/dataMask/actions — kept as literals so this test file has no
|
||||
// import dependency on those modules.
|
||||
test.each([
|
||||
'HYDRATE_DASHBOARD',
|
||||
'UPDATE_DATA_MASK',
|
||||
'SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE',
|
||||
])('onDidChangeDashboard fires on action type %s', actionType => {
|
||||
const listener = jest.fn();
|
||||
const disposable = dashboard.onDidChangeDashboard(listener);
|
||||
|
||||
dispatch(actionType);
|
||||
|
||||
expect(listener).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ dashboardId: 1, title: 'Sales' }),
|
||||
);
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('onDidChangeDashboard does not fire when not on dashboard page', () => {
|
||||
const { navigation } = jest.requireMock('../navigation');
|
||||
(navigation.getPageType as jest.Mock).mockReturnValue('explore');
|
||||
|
||||
const listener = jest.fn();
|
||||
const disposable = dashboard.onDidChangeDashboard(listener);
|
||||
dispatch('HYDRATE_DASHBOARD');
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
(navigation.getPageType as jest.Mock).mockReturnValue('dashboard');
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('disposed listener is not called', () => {
|
||||
const listener = jest.fn();
|
||||
const disposable = dashboard.onDidChangeDashboard(listener);
|
||||
disposable.dispose();
|
||||
dispatch('HYDRATE_DASHBOARD');
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1,123 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Host-internal implementation of the `dashboard` namespace.
|
||||
*
|
||||
* Wraps Redux dashboardInfo and dataMask state and normalizes them into the
|
||||
* stable `DashboardContext` contract. Extensions must not depend on the Redux
|
||||
* slice structure directly.
|
||||
*/
|
||||
|
||||
import type { dashboard as dashboardApi } from '@apache-superset/core';
|
||||
import type { DataMaskStateWithId } from '@superset-ui/core';
|
||||
import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
|
||||
import {
|
||||
UPDATE_DATA_MASK,
|
||||
SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE,
|
||||
} from 'src/dataMask/actions';
|
||||
import { store, RootState } from 'src/views/store';
|
||||
import { AnyListenerPredicate } from '@reduxjs/toolkit';
|
||||
import getChartIdsFromLayout from 'src/dashboard/util/getChartIdsFromLayout';
|
||||
import { createActionListener } from '../utils';
|
||||
import { navigation } from '../navigation';
|
||||
|
||||
type DashboardContext = dashboardApi.DashboardContext;
|
||||
type FilterValue = dashboardApi.FilterValue;
|
||||
type ChartSummary = NonNullable<DashboardContext['charts']>[number];
|
||||
|
||||
function buildChartSummaries(state: RootState): ChartSummary[] {
|
||||
const slices = state.sliceEntities?.slices ?? {};
|
||||
const layout = state.dashboardLayout?.present ?? {};
|
||||
|
||||
// Only charts actually placed on the dashboard layout — `slices` can also
|
||||
// hold entities that are not on the current dashboard.
|
||||
return getChartIdsFromLayout(layout).map(chartId => {
|
||||
const slice = slices[chartId];
|
||||
return {
|
||||
chartId,
|
||||
chartName: slice?.slice_name ?? '',
|
||||
vizType: slice?.viz_type ?? '',
|
||||
datasourceId: slice?.datasource_id ?? null,
|
||||
datasourceName: slice?.datasource_name ?? null,
|
||||
// Tab-accurate visibility is a deferred phase (SIP §10/§11); every chart
|
||||
// on the dashboard is reported visible for now.
|
||||
isVisible: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildDashboardContext(): DashboardContext | undefined {
|
||||
if (navigation.getPageType() !== 'dashboard') return undefined;
|
||||
// `store.getState()` is already typed as RootState, so the slices below are
|
||||
// read with their real types — the host owns this normalization and must
|
||||
// stay type-safe against slice reshapes.
|
||||
const state = store.getState();
|
||||
const info = state.dashboardInfo;
|
||||
if (!info?.id) return undefined;
|
||||
|
||||
const nativeFilters = state.nativeFilters?.filters ?? {};
|
||||
const dataMask: DataMaskStateWithId = state.dataMask ?? {};
|
||||
|
||||
const filters: FilterValue[] = Object.entries(dataMask)
|
||||
.filter(([id, mask]) => {
|
||||
if (!(id in nativeFilters)) return false;
|
||||
const value = mask?.filterState?.value;
|
||||
return value !== null && value !== undefined;
|
||||
})
|
||||
.map(([id, mask]) => {
|
||||
const raw = mask.filterState?.value;
|
||||
return {
|
||||
filterId: id,
|
||||
label: nativeFilters[id]?.name ?? id,
|
||||
value: Array.isArray(raw) ? [...raw] : raw,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
dashboardId: info.id,
|
||||
title: info.dashboard_title ?? info.slug ?? String(info.id),
|
||||
filters,
|
||||
charts: buildChartSummaries(state),
|
||||
};
|
||||
}
|
||||
|
||||
const dashboardChangePredicate: AnyListenerPredicate<RootState> = action =>
|
||||
action.type === HYDRATE_DASHBOARD ||
|
||||
action.type === UPDATE_DATA_MASK ||
|
||||
action.type === SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE;
|
||||
|
||||
const getCurrentDashboard: typeof dashboardApi.getCurrentDashboard = () =>
|
||||
buildDashboardContext();
|
||||
|
||||
const onDidChangeDashboard: typeof dashboardApi.onDidChangeDashboard = (
|
||||
listener: (ctx: DashboardContext) => void,
|
||||
thisArgs?: any,
|
||||
) =>
|
||||
createActionListener<DashboardContext>(
|
||||
dashboardChangePredicate,
|
||||
listener,
|
||||
() => buildDashboardContext() ?? null,
|
||||
thisArgs,
|
||||
);
|
||||
|
||||
export const dashboard: typeof dashboardApi = {
|
||||
getCurrentDashboard,
|
||||
onDidChangeDashboard,
|
||||
};
|
||||
@@ -1,63 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Host-internal implementation of the `dataset` namespace.
|
||||
*
|
||||
* Dataset page components call `setCurrentDataset` to publish context as they
|
||||
* load. Extensions consume the stable `DatasetContext` contract; they are
|
||||
* isolated from the page's internal data-fetching implementation.
|
||||
*/
|
||||
|
||||
import type { dataset as datasetApi } from '@apache-superset/core';
|
||||
import { createEmitter } from '../utils';
|
||||
|
||||
type DatasetContext = datasetApi.DatasetContext;
|
||||
|
||||
const emitter = createEmitter<DatasetContext | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Host-internal: called by the Dataset page when its entity loads or changes.
|
||||
* Not part of the public `@apache-superset/core` API.
|
||||
*/
|
||||
export const setCurrentDataset = (ctx: DatasetContext | undefined): void => {
|
||||
emitter.fire(ctx);
|
||||
};
|
||||
|
||||
const getCurrentDataset: typeof datasetApi.getCurrentDataset = () => {
|
||||
const current = emitter.getCurrent();
|
||||
return current ? { ...current } : undefined;
|
||||
};
|
||||
|
||||
const onDidChangeDataset: typeof datasetApi.onDidChangeDataset = (
|
||||
listener: (ctx: DatasetContext) => void,
|
||||
thisArgs?: unknown,
|
||||
) => {
|
||||
const bound = thisArgs ? listener.bind(thisArgs) : listener;
|
||||
// The public contract only emits a concrete context; skip `undefined` clears
|
||||
// so subscribers are never handed an empty value.
|
||||
return emitter.event(ctx => {
|
||||
if (ctx) bound(ctx);
|
||||
});
|
||||
};
|
||||
|
||||
export const dataset: typeof datasetApi = {
|
||||
getCurrentDataset,
|
||||
onDidChangeDataset,
|
||||
};
|
||||
@@ -1,157 +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.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Captured listeners — allows tests to trigger action notifications manually.
|
||||
// ---------------------------------------------------------------------------
|
||||
type ListenerEntry = {
|
||||
predicate: (action: { type: string }) => boolean;
|
||||
effect: (action: { type: string }) => void;
|
||||
};
|
||||
|
||||
const capturedListeners: ListenerEntry[] = [];
|
||||
|
||||
// Declared before jest.mock so the factory closure can reference it.
|
||||
let mockState: Record<string, unknown>;
|
||||
|
||||
jest.mock('src/views/store', () => ({
|
||||
store: { getState: () => mockState, dispatch: jest.fn() },
|
||||
listenerMiddleware: {
|
||||
startListening: (opts: {
|
||||
predicate: (action: { type: string }) => boolean;
|
||||
effect: (action: { type: string }) => void;
|
||||
}) => {
|
||||
const entry = { predicate: opts.predicate, effect: opts.effect };
|
||||
capturedListeners.push(entry);
|
||||
return () => {
|
||||
const idx = capturedListeners.indexOf(entry);
|
||||
if (idx !== -1) capturedListeners.splice(idx, 1);
|
||||
};
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../navigation', () => ({
|
||||
navigation: { getPageType: jest.fn(() => 'explore') },
|
||||
}));
|
||||
|
||||
function dispatch(actionType: string) {
|
||||
const action = { type: actionType };
|
||||
capturedListeners
|
||||
.filter(e => e.predicate(action))
|
||||
.forEach(e => e.effect(action));
|
||||
}
|
||||
|
||||
// Imported after mocks
|
||||
// eslint-disable-next-line import/first
|
||||
import { explore } from './index';
|
||||
|
||||
beforeEach(() => {
|
||||
mockState = {
|
||||
explore: {
|
||||
slice: { slice_id: 42, slice_name: 'My Chart' },
|
||||
datasource: { id: 7, table_name: 'orders' },
|
||||
controls: { viz_type: { value: 'bar' } },
|
||||
sliceName: 'My Chart',
|
||||
form_data: {},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
capturedListeners.length = 0;
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('getCurrentChart returns undefined when not on explore page', () => {
|
||||
const { navigation } = jest.requireMock('../navigation');
|
||||
(navigation.getPageType as jest.Mock).mockReturnValueOnce('dashboard');
|
||||
expect(explore.getCurrentChart()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getCurrentChart returns undefined when explore state is absent', () => {
|
||||
mockState = {};
|
||||
expect(explore.getCurrentChart()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getCurrentChart returns chart context from Redux state', () => {
|
||||
expect(explore.getCurrentChart()).toEqual({
|
||||
chartId: 42,
|
||||
chartName: 'My Chart',
|
||||
vizType: 'bar',
|
||||
datasourceId: 7,
|
||||
datasourceName: 'orders',
|
||||
});
|
||||
});
|
||||
|
||||
test('getCurrentChart returns null chartId for unsaved chart', () => {
|
||||
mockState = {
|
||||
explore: {
|
||||
slice: null,
|
||||
datasource: { id: 1, table_name: 'events' },
|
||||
controls: { viz_type: { value: 'line' } },
|
||||
sliceName: null,
|
||||
form_data: { viz_type: 'line' },
|
||||
},
|
||||
};
|
||||
expect(explore.getCurrentChart()?.chartId).toBeNull();
|
||||
});
|
||||
|
||||
// Action type strings match the constants in src/explore/actions/exploreActions
|
||||
// and src/explore/actions/datasourcesActions — kept as literals so this test
|
||||
// file has no import dependency on those modules.
|
||||
test.each([
|
||||
'HYDRATE_EXPLORE',
|
||||
'UPDATE_FORM_DATA', // SET_FORM_DATA constant resolves to this string
|
||||
'UPDATE_CHART_TITLE',
|
||||
'SET_DATASOURCE',
|
||||
'CREATE_NEW_SLICE',
|
||||
'SLICE_UPDATED',
|
||||
])('onDidChangeChart fires on action type %s', actionType => {
|
||||
const listener = jest.fn();
|
||||
const disposable = explore.onDidChangeChart(listener);
|
||||
|
||||
dispatch(actionType);
|
||||
|
||||
expect(listener).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ chartId: 42, vizType: 'bar' }),
|
||||
);
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('onDidChangeChart does not fire when page type is not explore', () => {
|
||||
const { navigation } = jest.requireMock('../navigation');
|
||||
(navigation.getPageType as jest.Mock).mockReturnValue('dashboard');
|
||||
|
||||
const listener = jest.fn();
|
||||
const disposable = explore.onDidChangeChart(listener);
|
||||
dispatch('HYDRATE_EXPLORE');
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
(navigation.getPageType as jest.Mock).mockReturnValue('explore');
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('disposed listener is not called', () => {
|
||||
const listener = jest.fn();
|
||||
const disposable = explore.onDidChangeChart(listener);
|
||||
disposable.dispose();
|
||||
dispatch('HYDRATE_EXPLORE');
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1,92 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Host-internal implementation of the `explore` namespace.
|
||||
*
|
||||
* Wraps Redux explore state and normalizes it into the stable `ChartContext`
|
||||
* contract. Extensions must not depend on the Redux slice structure directly.
|
||||
*/
|
||||
|
||||
import type { explore as exploreApi } from '@apache-superset/core';
|
||||
import { HYDRATE_EXPLORE } from 'src/explore/actions/hydrateExplore';
|
||||
import {
|
||||
CREATE_NEW_SLICE,
|
||||
SET_FORM_DATA,
|
||||
SLICE_UPDATED,
|
||||
UPDATE_CHART_TITLE,
|
||||
} from 'src/explore/actions/exploreActions';
|
||||
import { SET_DATASOURCE } from 'src/explore/actions/datasourcesActions';
|
||||
import { store, RootState } from 'src/views/store';
|
||||
import { AnyListenerPredicate } from '@reduxjs/toolkit';
|
||||
import { createActionListener } from '../utils';
|
||||
import { navigation } from '../navigation';
|
||||
|
||||
type ChartContext = exploreApi.ChartContext;
|
||||
|
||||
function buildChartContext(): ChartContext | undefined {
|
||||
if (navigation.getPageType() !== 'explore') return undefined;
|
||||
// `store.getState()` is already RootState; read the typed `explore` slice
|
||||
// directly rather than casting it away.
|
||||
const state = store.getState();
|
||||
const exploreState = state.explore;
|
||||
if (!exploreState) return undefined;
|
||||
|
||||
const { slice, datasource, controls } = exploreState;
|
||||
const vizType: string =
|
||||
(controls?.viz_type?.value as string) ??
|
||||
exploreState.form_data?.viz_type ??
|
||||
'';
|
||||
|
||||
return {
|
||||
chartId: slice?.slice_id ?? null,
|
||||
chartName: exploreState.sliceName ?? slice?.slice_name ?? null,
|
||||
vizType,
|
||||
datasourceId: datasource?.id ?? null,
|
||||
datasourceName:
|
||||
datasource?.table_name ?? datasource?.datasource_name ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const exploreChangePredicate: AnyListenerPredicate<RootState> = action =>
|
||||
action.type === HYDRATE_EXPLORE ||
|
||||
action.type === SET_FORM_DATA ||
|
||||
action.type === UPDATE_CHART_TITLE ||
|
||||
action.type === SET_DATASOURCE ||
|
||||
action.type === CREATE_NEW_SLICE ||
|
||||
action.type === SLICE_UPDATED;
|
||||
|
||||
const getCurrentChart: typeof exploreApi.getCurrentChart = () =>
|
||||
buildChartContext();
|
||||
|
||||
const onDidChangeChart: typeof exploreApi.onDidChangeChart = (
|
||||
listener: (ctx: ChartContext) => void,
|
||||
thisArgs?: any,
|
||||
) =>
|
||||
createActionListener<ChartContext>(
|
||||
exploreChangePredicate,
|
||||
listener,
|
||||
() => buildChartContext() ?? null,
|
||||
thisArgs,
|
||||
);
|
||||
|
||||
export const explore: typeof exploreApi = {
|
||||
getCurrentChart,
|
||||
onDidChangeChart,
|
||||
};
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { extensions as extensionsApi } from '@apache-superset/core';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import ExtensionsLoader from 'src/extensions/ExtensionsLoader';
|
||||
|
||||
const getExtension: typeof extensionsApi.getExtension = id =>
|
||||
@@ -30,61 +29,3 @@ export const extensions: typeof extensionsApi = {
|
||||
getExtension,
|
||||
getAllExtensions,
|
||||
};
|
||||
|
||||
/**
|
||||
* Deployment-wide extension admin settings. The keys are snake_case to match
|
||||
* the `/api/v1/extensions/settings` wire shape this store loads from.
|
||||
* Settings are read-only from the frontend; the admin write path has been
|
||||
* removed in favour of direct backend configuration.
|
||||
*/
|
||||
export type ExtensionSettings = {
|
||||
active_chatbot_id: string | null;
|
||||
};
|
||||
|
||||
const SETTINGS_ENDPOINT = '/api/v1/extensions/settings';
|
||||
|
||||
const EMPTY_SETTINGS: ExtensionSettings = {
|
||||
active_chatbot_id: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Single module-level store for extension admin settings. The chatbot mount
|
||||
* reads this one source via `useSyncExternalStore` so it re-resolves when the
|
||||
* store is updated — no bespoke second notification channel needed.
|
||||
*/
|
||||
let settings: ExtensionSettings = EMPTY_SETTINGS;
|
||||
const settingsListeners = new Set<() => void>();
|
||||
|
||||
const emitSettingsChange = (): void => {
|
||||
settingsListeners.forEach(fn => fn());
|
||||
};
|
||||
|
||||
/** Subscribe to settings changes (for `useSyncExternalStore`). */
|
||||
export const subscribeToExtensionSettings = (
|
||||
listener: () => void,
|
||||
): (() => void) => {
|
||||
settingsListeners.add(listener);
|
||||
return () => {
|
||||
settingsListeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
/** Current settings snapshot (for `useSyncExternalStore`). */
|
||||
export const getExtensionSettingsSnapshot = (): ExtensionSettings => settings;
|
||||
|
||||
/** Replace the settings snapshot and notify subscribers. Module-private; only loadExtensionSettings should call this. */
|
||||
const applyExtensionSettings = (next: ExtensionSettings): void => {
|
||||
settings = next;
|
||||
emitSettingsChange();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch settings from the server into the store. Resolves to the loaded value;
|
||||
* on failure the store is left untouched and the error is rethrown so callers
|
||||
* can surface it.
|
||||
*/
|
||||
export const loadExtensionSettings = async (): Promise<ExtensionSettings> => {
|
||||
const { json } = await SupersetClient.get({ endpoint: SETTINGS_ENDPOINT });
|
||||
applyExtensionSettings(json.result ?? EMPTY_SETTINGS);
|
||||
return settings;
|
||||
};
|
||||
|
||||
@@ -28,14 +28,10 @@ export const core: typeof coreType = {
|
||||
|
||||
export * from './authentication';
|
||||
export * from './commands';
|
||||
export * from './dashboard';
|
||||
export * from './dataset';
|
||||
export * from './editors';
|
||||
export * from './explore';
|
||||
export * from './extensions';
|
||||
export * from './menus';
|
||||
export * from './models';
|
||||
export * from './navigation';
|
||||
export * from './sqlLab';
|
||||
export * from './utils';
|
||||
export * from './views';
|
||||
|
||||
@@ -1,121 +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.
|
||||
*/
|
||||
|
||||
// Reset module state between tests so currentPageType is re-initialized.
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: { pathname: '/' },
|
||||
});
|
||||
});
|
||||
|
||||
async function importNavigation() {
|
||||
const mod = await import('./index');
|
||||
return mod;
|
||||
}
|
||||
|
||||
test('getPageType returns "other" for unknown pathname', async () => {
|
||||
const { navigation } = await importNavigation();
|
||||
expect(navigation.getPageType()).toBe('other');
|
||||
});
|
||||
|
||||
test('getPageType derives page type from window.location.pathname', async () => {
|
||||
window.location.pathname = '/superset/dashboard/42/';
|
||||
const { navigation } = await importNavigation();
|
||||
expect(navigation.getPageType()).toBe('dashboard');
|
||||
});
|
||||
|
||||
test('notifyPageChange updates the current page type', async () => {
|
||||
const { navigation, notifyPageChange } = await importNavigation();
|
||||
notifyPageChange('/explore/?form_data={}');
|
||||
expect(navigation.getPageType()).toBe('explore');
|
||||
});
|
||||
|
||||
test('notifyPageChange fires listeners on page type change', async () => {
|
||||
const { navigation, notifyPageChange } = await importNavigation();
|
||||
const listener = jest.fn();
|
||||
const disposable = navigation.onDidChangePage(listener);
|
||||
|
||||
notifyPageChange('/superset/dashboard/1/');
|
||||
expect(listener).toHaveBeenCalledWith('dashboard');
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('notifyPageChange does not fire listeners when page type is unchanged', async () => {
|
||||
window.location.pathname = '/superset/dashboard/1/';
|
||||
const { navigation, notifyPageChange } = await importNavigation();
|
||||
const listener = jest.fn();
|
||||
navigation.onDidChangePage(listener);
|
||||
|
||||
notifyPageChange('/superset/dashboard/2/');
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('onDidChangePage listener is removed after dispose', async () => {
|
||||
const { navigation, notifyPageChange } = await importNavigation();
|
||||
const listener = jest.fn();
|
||||
const disposable = navigation.onDidChangePage(listener);
|
||||
|
||||
disposable.dispose();
|
||||
notifyPageChange('/superset/dashboard/1/');
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('sqllab path is matched with and without trailing slash', async () => {
|
||||
const { notifyPageChange, navigation } = await importNavigation();
|
||||
notifyPageChange('/sqllab');
|
||||
expect(navigation.getPageType()).toBe('sqllab');
|
||||
notifyPageChange('/explore/');
|
||||
notifyPageChange('/sqllab/');
|
||||
expect(navigation.getPageType()).toBe('sqllab');
|
||||
});
|
||||
|
||||
test('chart and dashboard list pages get their own page types', async () => {
|
||||
const { notifyPageChange, navigation } = await importNavigation();
|
||||
notifyPageChange('/chart/list/');
|
||||
expect(navigation.getPageType()).toBe('chart_list');
|
||||
notifyPageChange('/dashboard/list/');
|
||||
expect(navigation.getPageType()).toBe('dashboard_list');
|
||||
});
|
||||
|
||||
test('dataset list and single-dataset pages get distinct page types', async () => {
|
||||
const { notifyPageChange, navigation } = await importNavigation();
|
||||
notifyPageChange('/tablemodelview/list/');
|
||||
expect(navigation.getPageType()).toBe('dataset_list');
|
||||
notifyPageChange('/dataset/42');
|
||||
expect(navigation.getPageType()).toBe('dataset');
|
||||
});
|
||||
|
||||
test('sqllab editor, query history, and saved queries get distinct page types', async () => {
|
||||
const { notifyPageChange, navigation } = await importNavigation();
|
||||
notifyPageChange('/sqllab/');
|
||||
expect(navigation.getPageType()).toBe('sqllab');
|
||||
notifyPageChange('/sqllab/history/');
|
||||
expect(navigation.getPageType()).toBe('query_history');
|
||||
notifyPageChange('/savedqueryview/list/');
|
||||
expect(navigation.getPageType()).toBe('saved_queries');
|
||||
});
|
||||
|
||||
test('chart/add resolves to explore, not chart_list', async () => {
|
||||
const { notifyPageChange, navigation } = await importNavigation();
|
||||
notifyPageChange('/chart/add');
|
||||
expect(navigation.getPageType()).toBe('explore');
|
||||
});
|
||||
@@ -1,82 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Host-internal implementation of the `navigation` namespace.
|
||||
*
|
||||
* Backed by browser location — no Redux dependency.
|
||||
* The app shell calls `notifyPageChange(pathname)` whenever the route changes.
|
||||
*/
|
||||
|
||||
import type { navigation as navigationApi } from '@apache-superset/core';
|
||||
import { Disposable } from '../models';
|
||||
|
||||
type PageType = navigationApi.PageType;
|
||||
|
||||
const listeners = new Set<(pageType: PageType) => void>();
|
||||
|
||||
function derivePageType(pathname: string): PageType {
|
||||
if (pathname.startsWith('/superset/dashboard/')) return 'dashboard';
|
||||
if (pathname.startsWith('/dashboard/list')) return 'dashboard_list';
|
||||
if (pathname.startsWith('/explore/')) return 'explore';
|
||||
if (pathname.startsWith('/superset/explore/')) return 'explore';
|
||||
if (pathname.startsWith('/chart/add')) return 'explore';
|
||||
if (pathname.startsWith('/chart/list')) return 'chart_list';
|
||||
if (pathname.startsWith('/sqllab/history')) return 'query_history';
|
||||
if (pathname.startsWith('/savedqueryview/list')) return 'saved_queries';
|
||||
if (pathname === '/sqllab' || pathname.startsWith('/sqllab/'))
|
||||
return 'sqllab';
|
||||
if (pathname.startsWith('/tablemodelview/list')) return 'dataset_list';
|
||||
if (pathname.startsWith('/dataset/')) return 'dataset';
|
||||
if (pathname.startsWith('/superset/welcome/')) return 'home';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
let currentPageType: PageType | undefined;
|
||||
|
||||
function getOrInitPageType(): PageType {
|
||||
if (currentPageType === undefined) {
|
||||
currentPageType = derivePageType(window.location.pathname);
|
||||
}
|
||||
return currentPageType;
|
||||
}
|
||||
|
||||
/** Called by ExtensionsStartup whenever the React Router location changes. */
|
||||
export const notifyPageChange = (pathname: string): void => {
|
||||
const next = derivePageType(pathname);
|
||||
if (next === getOrInitPageType()) return;
|
||||
currentPageType = next;
|
||||
listeners.forEach(fn => fn(next));
|
||||
};
|
||||
|
||||
const getPageType: typeof navigationApi.getPageType = () => getOrInitPageType();
|
||||
|
||||
const onDidChangePage: typeof navigationApi.onDidChangePage = (
|
||||
listener: (pageType: PageType) => void,
|
||||
thisArgs?: any,
|
||||
): Disposable => {
|
||||
const bound = thisArgs ? listener.bind(thisArgs) : listener;
|
||||
listeners.add(bound);
|
||||
return new Disposable(() => listeners.delete(bound));
|
||||
};
|
||||
|
||||
export const navigation: typeof navigationApi = {
|
||||
getPageType,
|
||||
onDidChangePage,
|
||||
};
|
||||
@@ -56,7 +56,6 @@ import {
|
||||
QueryResultContext,
|
||||
QueryErrorResultContext,
|
||||
} from './models';
|
||||
import { navigation } from '../navigation';
|
||||
|
||||
const { CTASMethod } = sqlLabApi;
|
||||
|
||||
@@ -302,15 +301,8 @@ function createQueryErrorContext(
|
||||
);
|
||||
}
|
||||
|
||||
const getCurrentTab: typeof sqlLabApi.getCurrentTab = () => {
|
||||
// Guard on the page type so the tab does not leak onto non-editor surfaces.
|
||||
// The SQL Lab Redux slice persists after navigating away, so without this
|
||||
// guard `getCurrentTab()` would keep returning the last editor's tab on, e.g.,
|
||||
// a dashboard or list page. Mirrors the page-type guards on
|
||||
// `explore.getCurrentChart()` / `dashboard.getCurrentDashboard()`.
|
||||
if (navigation.getPageType() !== 'sqllab') return undefined;
|
||||
return getTab(activeEditorId());
|
||||
};
|
||||
const getCurrentTab: typeof sqlLabApi.getCurrentTab = () =>
|
||||
getTab(activeEditorId());
|
||||
|
||||
const getActivePanel: typeof sqlLabApi.getActivePanel = () => {
|
||||
const { activeSouthPaneTab } = getSqlLabState();
|
||||
@@ -460,14 +452,8 @@ const onDidChangeActiveTab: typeof sqlLabApi.onDidChangeActiveTab = (
|
||||
createActionListener(
|
||||
globalPredicate(SET_ACTIVE_QUERY_EDITOR),
|
||||
listener,
|
||||
// Resolve the now-active tab the same way `getCurrentTab()` does (via the
|
||||
// active-editor / tabHistory state) rather than from the raw action payload.
|
||||
// The action's `queryEditor` carries the base editor without `unsavedQueryEditor`
|
||||
// merged, so its `dbId` can still be undefined at this point, which made
|
||||
// `getTab(action.queryEditor.id)` return undefined and silently swallow the
|
||||
// event. Reading the resolved active tab keeps this event consistent with the
|
||||
// getter and fires on every tab switch.
|
||||
() => getCurrentTab() ?? null,
|
||||
(action: { type: string; queryEditor: { id: string } }) =>
|
||||
getTab(action.queryEditor.id),
|
||||
thisArgs,
|
||||
);
|
||||
|
||||
|
||||
@@ -119,13 +119,6 @@ jest.mock('src/views/store', () => ({
|
||||
setupStore: jest.fn(),
|
||||
}));
|
||||
|
||||
// The sqlLab namespace guards `getCurrentTab()` on the page type. These tests
|
||||
// exercise the editor surface, so report 'sqllab'. Per-test overrides (e.g. to
|
||||
// assert the off-surface guard) can change the return value.
|
||||
jest.mock('../navigation', () => ({
|
||||
navigation: { getPageType: jest.fn(() => 'sqllab') },
|
||||
}));
|
||||
|
||||
// Module under test — imported after mocks
|
||||
// eslint-disable-next-line import/first
|
||||
import { sqlLab } from '.';
|
||||
@@ -395,31 +388,6 @@ test('onDidChangeActiveTab fires with Tab on SET_ACTIVE_QUERY_EDITOR', () => {
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('onDidChangeActiveTab carries the newly-activated tab when switching away', () => {
|
||||
// Switching from the first editor to a second one must report the second tab,
|
||||
// not the first. Regression guard: resolving the tab from the live active
|
||||
// editor (via getCurrentTab) instead of the raw action payload.
|
||||
mockStore.dispatch({
|
||||
type: ADD_QUERY_EDITOR,
|
||||
queryEditor: makeSecondEditor(),
|
||||
});
|
||||
|
||||
const listener = jest.fn();
|
||||
const disposable = sqlLab.onDidChangeActiveTab(listener);
|
||||
|
||||
mockStore.dispatch({
|
||||
type: SET_ACTIVE_QUERY_EDITOR,
|
||||
queryEditor: { id: 'editor-2' },
|
||||
});
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
const tab = listener.mock.calls[0][0];
|
||||
expect(tab.id).toBe('editor-2');
|
||||
expect(tab.databaseId).toBe(2);
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('onDidCreateTab fires with Tab on ADD_QUERY_EDITOR', () => {
|
||||
const listener = jest.fn();
|
||||
const disposable = sqlLab.onDidCreateTab(listener);
|
||||
@@ -567,13 +535,6 @@ test('getCurrentTab returns the active tab with correct properties', () => {
|
||||
expect(tab!.schema).toBe('public');
|
||||
});
|
||||
|
||||
test('getCurrentTab returns undefined when not on the SQL Lab editor surface', () => {
|
||||
const { navigation } = jest.requireMock('../navigation');
|
||||
(navigation.getPageType as jest.Mock).mockReturnValueOnce('dashboard');
|
||||
|
||||
expect(sqlLab.getCurrentTab()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getActivePanel returns the active south pane tab', () => {
|
||||
const panel = sqlLab.getActivePanel();
|
||||
expect(panel.id).toBe('Results');
|
||||
|
||||
@@ -20,56 +20,6 @@ import type { common as core } from '@apache-superset/core';
|
||||
import { AnyAction } from 'redux';
|
||||
import { listenerMiddleware, RootState, store } from 'src/views/store';
|
||||
import { AnyListenerPredicate } from '@reduxjs/toolkit';
|
||||
import { Disposable } from './models';
|
||||
|
||||
/**
|
||||
* A typed event subscription matching the public `Event<T>` contract.
|
||||
* Calling it with a listener (and optional `this` arg) subscribes and returns
|
||||
* a {@link Disposable} that unsubscribes.
|
||||
*/
|
||||
export type EventSubscriber<T> = (
|
||||
listener: (e: T) => void,
|
||||
thisArgs?: unknown,
|
||||
) => Disposable;
|
||||
|
||||
/**
|
||||
* A minimal host-internal event emitter shared by the producer-backed
|
||||
* namespaces (dataset, navigation, settings, view registry). Each of those
|
||||
* needs the same "publish a value and fan it out to subscribers" primitive;
|
||||
* this collapses the duplicated Set + bind + Disposable boilerplate into one
|
||||
* place.
|
||||
*
|
||||
* `event` is exposed to extensions as the namespace's `onDidChange*`; `fire`
|
||||
* and `getCurrent` stay host-internal.
|
||||
*/
|
||||
export interface Emitter<T> {
|
||||
/** Subscribe to changes; conforms to the public `Event<T>` shape. */
|
||||
event: EventSubscriber<T>;
|
||||
/** Notify all current subscribers with `value`. */
|
||||
fire: (value: T) => void;
|
||||
/** The most recently fired value (or the initial value). */
|
||||
getCurrent: () => T;
|
||||
}
|
||||
|
||||
export function createEmitter<T>(initial: T): Emitter<T> {
|
||||
const listeners = new Set<(e: T) => void>();
|
||||
let current = initial;
|
||||
|
||||
return {
|
||||
event: (listener, thisArgs) => {
|
||||
const bound = thisArgs ? listener.bind(thisArgs) : listener;
|
||||
listeners.add(bound);
|
||||
return new Disposable(() => {
|
||||
listeners.delete(bound);
|
||||
});
|
||||
},
|
||||
fire: value => {
|
||||
current = value;
|
||||
listeners.forEach(fn => fn(value));
|
||||
},
|
||||
getCurrent: () => current,
|
||||
};
|
||||
}
|
||||
|
||||
export function createActionListener<V>(
|
||||
predicate: AnyListenerPredicate<RootState>,
|
||||
|
||||
@@ -17,12 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
views,
|
||||
resolveView,
|
||||
getViewProvider,
|
||||
getRegisteredViewIds,
|
||||
} from './index';
|
||||
import { views, resolveView } from './index';
|
||||
|
||||
const disposables: Array<{ dispose: () => void }> = [];
|
||||
|
||||
@@ -115,59 +110,3 @@ test('dispose removes the view registration', () => {
|
||||
|
||||
expect(views.getViews('sqllab.panels')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getViewProvider returns the registered provider for a matching location', () => {
|
||||
const provider = () => React.createElement('div', null, 'Test');
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'test.provider', name: 'Test Provider' },
|
||||
'core.chatbot',
|
||||
provider,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getViewProvider('core.chatbot', 'test.provider')).toBe(provider);
|
||||
});
|
||||
|
||||
test('getViewProvider returns undefined when the location does not match', () => {
|
||||
const provider = () => React.createElement('div', null, 'Test');
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'test.provider', name: 'Test Provider' },
|
||||
'sqllab.panels',
|
||||
provider,
|
||||
),
|
||||
);
|
||||
|
||||
// Registered, but at a different location.
|
||||
expect(getViewProvider('core.chatbot', 'test.provider')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getViewProvider returns undefined for an unknown id', () => {
|
||||
expect(getViewProvider('core.chatbot', 'nonexistent')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getRegisteredViewIds returns ids in registration order', () => {
|
||||
const provider = () => React.createElement('div', null, 'Test');
|
||||
disposables.push(
|
||||
views.registerView(
|
||||
{ id: 'first.chatbot', name: 'First' },
|
||||
'core.chatbot',
|
||||
provider,
|
||||
),
|
||||
views.registerView(
|
||||
{ id: 'second.chatbot', name: 'Second' },
|
||||
'core.chatbot',
|
||||
provider,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getRegisteredViewIds('core.chatbot')).toEqual([
|
||||
'first.chatbot',
|
||||
'second.chatbot',
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRegisteredViewIds returns an empty array for an unused location', () => {
|
||||
expect(getRegisteredViewIds('core.chatbot')).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -39,27 +39,6 @@ const viewRegistry: Map<
|
||||
|
||||
const locationIndex: Map<string, Set<string>> = new Map();
|
||||
|
||||
/**
|
||||
* Monotonic version of the view registry. Bumped on every registration or
|
||||
* disposal so consumers can re-derive state via React's `useSyncExternalStore`.
|
||||
*/
|
||||
let registryVersion = 0;
|
||||
const registrySubscribers = new Set<() => void>();
|
||||
|
||||
const notifyRegistry = () => {
|
||||
registryVersion += 1;
|
||||
registrySubscribers.forEach(fn => fn());
|
||||
};
|
||||
|
||||
export const subscribeToRegistry = (listener: () => void): (() => void) => {
|
||||
registrySubscribers.add(listener);
|
||||
return () => {
|
||||
registrySubscribers.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
export const getRegistryVersion = () => registryVersion;
|
||||
|
||||
const registerView: typeof viewsApi.registerView = (
|
||||
view: View,
|
||||
location: string,
|
||||
@@ -67,24 +46,15 @@ const registerView: typeof viewsApi.registerView = (
|
||||
): Disposable => {
|
||||
const { id } = view;
|
||||
|
||||
const previousLocation = viewRegistry.get(id)?.location;
|
||||
if (previousLocation && previousLocation !== location) {
|
||||
locationIndex.get(previousLocation)?.delete(id);
|
||||
}
|
||||
|
||||
viewRegistry.set(id, { view, location, provider });
|
||||
|
||||
const ids = locationIndex.get(location) ?? new Set();
|
||||
ids.add(id);
|
||||
locationIndex.set(location, ids);
|
||||
|
||||
notifyRegistry();
|
||||
|
||||
return new Disposable(() => {
|
||||
const registeredLocation = viewRegistry.get(id)?.location ?? location;
|
||||
viewRegistry.delete(id);
|
||||
locationIndex.get(registeredLocation)?.delete(id);
|
||||
notifyRegistry();
|
||||
locationIndex.get(location)?.delete(id);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -107,28 +77,6 @@ const getViews: typeof viewsApi.getViews = (
|
||||
.filter((c): c is View => !!c);
|
||||
};
|
||||
|
||||
/**
|
||||
* Host-internal: returns the provider for a registered view id at a location.
|
||||
* Not part of the public `@apache-superset/core` API — `getViews` stays
|
||||
* descriptor-only so extensions cannot render each other's views directly.
|
||||
*/
|
||||
export const getViewProvider = (
|
||||
location: string,
|
||||
id: string,
|
||||
): (() => ReactElement) | undefined => {
|
||||
const entry = viewRegistry.get(id);
|
||||
if (entry?.location !== location) {
|
||||
return undefined;
|
||||
}
|
||||
return entry.provider;
|
||||
};
|
||||
|
||||
/** Host-internal: view ids at a location in registration order. */
|
||||
export const getRegisteredViewIds = (location: string): string[] => {
|
||||
const ids = locationIndex.get(location);
|
||||
return ids ? Array.from(ids) : [];
|
||||
};
|
||||
|
||||
export const views: typeof viewsApi = {
|
||||
registerView,
|
||||
getViews,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 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 { SupersetClient, isFeatureEnabled } from '@superset-ui/core';
|
||||
import { waitFor } from 'spec/helpers/testing-library';
|
||||
import {
|
||||
filterId,
|
||||
sliceEntitiesForDashboard as sliceEntities,
|
||||
} from 'spec/fixtures/mockSliceEntities';
|
||||
import { emptyFilters } from 'spec/fixtures/mockDashboardFilters';
|
||||
import mockDashboardData from 'spec/fixtures/mockDashboardData';
|
||||
import { saveDashboardRequest } from 'src/dashboard/actions/dashboardState';
|
||||
import { SAVE_TYPE_OVERWRITE } from 'src/dashboard/util/constants';
|
||||
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
...jest.requireActual('@superset-ui/core'),
|
||||
isFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockIsFeatureEnabled = isFeatureEnabled as jest.Mock;
|
||||
|
||||
const mockState = {
|
||||
dashboardState: { sliceIds: [filterId], hasUnsavedChanges: true },
|
||||
dashboardInfo: { metadata: { color_scheme: 'supersetColors' } },
|
||||
sliceEntities,
|
||||
dashboardFilters: emptyFilters,
|
||||
dashboardLayout: {
|
||||
past: [],
|
||||
present: mockDashboardData.positions,
|
||||
future: {},
|
||||
},
|
||||
charts: {},
|
||||
};
|
||||
|
||||
let putStub: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Disable ConfirmDashboardDiff so SAVE_TYPE_OVERWRITE always calls PUT
|
||||
// directly (skipping the GET precheck) — without this the test outcome
|
||||
// depends on the global feature-flag state and the assertions become
|
||||
// non-deterministic, meaning a reverted fix may go undetected.
|
||||
mockIsFeatureEnabled.mockReturnValue(false);
|
||||
jest.spyOn(SupersetClient, 'post').mockResolvedValue({} as any);
|
||||
jest.spyOn(SupersetClient, 'get').mockResolvedValue({} as any);
|
||||
putStub = jest.spyOn(SupersetClient, 'put').mockResolvedValue({
|
||||
json: { result: mockDashboardData },
|
||||
} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const getState = jest.fn(() => mockState) as unknown as () => any;
|
||||
const dispatch = jest.fn();
|
||||
return { getState, dispatch };
|
||||
}
|
||||
|
||||
test('clears certification_details when certified_by is cleared', async () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const thunk = saveDashboardRequest(
|
||||
{
|
||||
...mockDashboardData,
|
||||
certified_by: '',
|
||||
certification_details: 'Old details',
|
||||
},
|
||||
1,
|
||||
SAVE_TYPE_OVERWRITE,
|
||||
);
|
||||
thunk(dispatch, getState);
|
||||
await waitFor(() => expect(putStub.mock.calls.length).toBe(1));
|
||||
const body = JSON.parse(putStub.mock.calls[0][0].body);
|
||||
expect(body.certified_by).toBe('');
|
||||
expect(body.certification_details).toBe('');
|
||||
});
|
||||
|
||||
test('preserves certification_details when certified_by is set', async () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const thunk = saveDashboardRequest(
|
||||
{
|
||||
...mockDashboardData,
|
||||
certified_by: 'Alice',
|
||||
certification_details: 'Verified by Alice',
|
||||
},
|
||||
1,
|
||||
SAVE_TYPE_OVERWRITE,
|
||||
);
|
||||
thunk(dispatch, getState);
|
||||
await waitFor(() => expect(putStub.mock.calls.length).toBe(1));
|
||||
const body = JSON.parse(putStub.mock.calls[0][0].body);
|
||||
expect(body.certified_by).toBe('Alice');
|
||||
expect(body.certification_details).toBe('Verified by Alice');
|
||||
});
|
||||
|
||||
test('omits certification fields when certified_by is undefined', async () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const thunk = saveDashboardRequest(
|
||||
{
|
||||
...mockDashboardData,
|
||||
certified_by: undefined,
|
||||
certification_details: undefined,
|
||||
},
|
||||
1,
|
||||
SAVE_TYPE_OVERWRITE,
|
||||
);
|
||||
thunk(dispatch, getState);
|
||||
await waitFor(() => expect(putStub.mock.calls.length).toBe(1));
|
||||
const body = JSON.parse(putStub.mock.calls[0][0].body);
|
||||
expect(body).not.toHaveProperty('certified_by');
|
||||
expect(body).not.toHaveProperty('certification_details');
|
||||
});
|
||||
@@ -902,3 +902,61 @@ test('Clear All on a required filter disables Apply via validateStatus', async (
|
||||
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
|
||||
updateDataMaskSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('FilterBar renders the configured filter name in the bar', async () => {
|
||||
const filterId = 'NATIVE_FILTER-name-render';
|
||||
const filter = createFilter({
|
||||
id: filterId,
|
||||
name: 'Region',
|
||||
filterType: 'filter_select',
|
||||
targets: [{ datasetId: 7, column: { name: 'region' } }],
|
||||
chartsInScope: [18],
|
||||
});
|
||||
const state = {
|
||||
...stateWithoutNativeFilters,
|
||||
dashboardInfo: {
|
||||
id: 1,
|
||||
dash_edit_perm: true,
|
||||
filterBarOrientation: FilterBarOrientation.Vertical,
|
||||
metadata: {
|
||||
native_filter_configuration: [filter],
|
||||
chart_configuration: {},
|
||||
},
|
||||
},
|
||||
dashboardState: {
|
||||
...stateWithoutNativeFilters.dashboardState,
|
||||
activeTabs: ['ROOT_ID'],
|
||||
},
|
||||
dataMask: { [filterId]: createDataMask(filterId, undefined, {}) },
|
||||
nativeFilters: {
|
||||
filters: { [filterId]: filter },
|
||||
filtersState: {},
|
||||
},
|
||||
};
|
||||
|
||||
const props = createOpenedBarProps();
|
||||
renderFilterBar(props, state);
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
expect(await screen.findByText('Region')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Clicking the gear "Add or edit filters and controls" item opens the FiltersConfigModal', async () => {
|
||||
const props = createOpenedBarProps();
|
||||
renderFilterBar(props, stateWithoutNativeFilters);
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
|
||||
const gear = await screen.findByTestId('filterbar-orientation-icon');
|
||||
userEvent.click(gear);
|
||||
|
||||
const addEditItem = await screen.findByText(
|
||||
'Add or edit filters and controls',
|
||||
);
|
||||
userEvent.click(addEditItem);
|
||||
|
||||
expect(await screen.findByTestId('filter-modal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -844,9 +844,7 @@ test('enables save button and includes updated title when editing an existing di
|
||||
jest.useRealTimers();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('button', { name: SAVE_REGEX }),
|
||||
).not.toBeDisabled(),
|
||||
expect(screen.getByRole('button', { name: SAVE_REGEX })).not.toBeDisabled(),
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
|
||||
@@ -910,9 +908,7 @@ test('enables save button and includes updated title when editing an existing ch
|
||||
jest.useRealTimers();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('button', { name: SAVE_REGEX }),
|
||||
).not.toBeDisabled(),
|
||||
expect(screen.getByRole('button', { name: SAVE_REGEX })).not.toBeDisabled(),
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
|
||||
@@ -960,3 +956,144 @@ test('empty state disappears when a filter is added via dropdown', async () => {
|
||||
});
|
||||
expect(screen.getByText(FILTER_TYPE_REGEX)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('restores a deleted filter via the "Restore filter" button', async () => {
|
||||
const nativeFilterConfig = [
|
||||
buildNativeFilter('NATIVE_FILTER-1', 'state', []),
|
||||
buildNativeFilter('NATIVE_FILTER-2', 'country', []),
|
||||
];
|
||||
const state = {
|
||||
...defaultState(),
|
||||
dashboardInfo: {
|
||||
metadata: { native_filter_configuration: nativeFilterConfig },
|
||||
},
|
||||
dashboardLayout,
|
||||
};
|
||||
|
||||
defaultRender(state, { ...props, createNewOnOpen: false });
|
||||
|
||||
const filterContainer = screen.getByTestId('filter-title-container');
|
||||
const firstTab = within(filterContainer).getAllByRole('tab')[0];
|
||||
fireEvent.click(within(firstTab).getByRole('button', { name: /delete/i }));
|
||||
|
||||
expect(
|
||||
await screen.findByText(/you have removed this filter/i),
|
||||
).toBeInTheDocument();
|
||||
const restoreButton = screen.getByTestId('restore-filter-button');
|
||||
await userEvent.click(restoreButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/you have removed this filter/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('textbox', { name: FILTER_NAME_REGEX })).toHaveValue(
|
||||
'state',
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
test('undoes a filter deletion via the sidebar "Undo?" link', async () => {
|
||||
const nativeFilterConfig = [
|
||||
buildNativeFilter('NATIVE_FILTER-1', 'state', []),
|
||||
buildNativeFilter('NATIVE_FILTER-2', 'country', []),
|
||||
];
|
||||
const state = {
|
||||
...defaultState(),
|
||||
dashboardInfo: {
|
||||
metadata: { native_filter_configuration: nativeFilterConfig },
|
||||
},
|
||||
dashboardLayout,
|
||||
};
|
||||
|
||||
defaultRender(state, { ...props, createNewOnOpen: false });
|
||||
|
||||
const filterContainer = screen.getByTestId('filter-title-container');
|
||||
const firstTab = within(filterContainer).getAllByRole('tab')[0];
|
||||
fireEvent.click(within(firstTab).getByRole('button', { name: /delete/i }));
|
||||
|
||||
const undoButton = await screen.findByTestId('undo-button');
|
||||
expect(undoButton).toHaveTextContent(/undo\?/i);
|
||||
await userEvent.click(undoButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/you have removed this filter/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('textbox', { name: FILTER_NAME_REGEX })).toHaveValue(
|
||||
'state',
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
test('shows info tooltips beside value-filter options and reveals tooltip text on hover', async () => {
|
||||
defaultRender();
|
||||
|
||||
// Upstream Cypress checked six tooltips on the value filter (nativeFilterTooltips
|
||||
// 0..5); asserting the count keeps this test honest if tooltips get added or
|
||||
// removed alongside a regression to the option list.
|
||||
const tooltipIcons = screen.getAllByLabelText(/show info tooltip/i);
|
||||
expect(tooltipIcons.length).toBeGreaterThanOrEqual(6);
|
||||
|
||||
await userEvent.hover(tooltipIcons[0]);
|
||||
|
||||
// role='tooltip' trips an nwsapi bug on antd's internal :only-child selectors;
|
||||
// query the portal node by class and require non-empty text content so an empty
|
||||
// tooltip render does not pass.
|
||||
await waitFor(() => {
|
||||
const tooltip = document.querySelector('.ant-tooltip-inner');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(tooltip?.textContent?.trim()).toBeTruthy();
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
test('numerical range filter — Range Type selector lets the user pick a display mode', async () => {
|
||||
defaultRender();
|
||||
|
||||
await userEvent.click(screen.getByText(VALUE_REGEX));
|
||||
await userEvent.click(await screen.findByText(NUMERICAL_RANGE_REGEX));
|
||||
|
||||
const rangeTypeCombobox = await screen.findByRole('combobox', {
|
||||
name: /range type/i,
|
||||
});
|
||||
|
||||
// Default render is "Slider and range input"; asserting Slider is absent first
|
||||
// ensures the post-click assertion proves a state change rather than passing on
|
||||
// the default selection.
|
||||
expect(
|
||||
document.querySelector('.ant-select-selection-item[title="Slider"]'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(rangeTypeCombobox);
|
||||
const sliderOption = await screen.findByRole('option', {
|
||||
name: /^slider$/i,
|
||||
});
|
||||
await userEvent.click(sliderOption);
|
||||
|
||||
// antd Select renders the active selection as a span whose title attribute is
|
||||
// the picked option's label.
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
document.querySelector('.ant-select-selection-item[title="Slider"]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
test('toggles "Filter has default value" to show and hide the Default Value control', async () => {
|
||||
defaultRender();
|
||||
|
||||
const defaultValueCheckbox = getCheckbox(DEFAULT_VALUE_REGEX);
|
||||
expect(defaultValueCheckbox).not.toBeChecked();
|
||||
expect(screen.queryByText(/^default value$/i)).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(defaultValueCheckbox);
|
||||
|
||||
expect(defaultValueCheckbox).toBeChecked();
|
||||
expect(await screen.findByText(/^default value$/i)).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(defaultValueCheckbox);
|
||||
|
||||
expect(defaultValueCheckbox).not.toBeChecked();
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/^default value$/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,6 +84,14 @@ test('the form validates required fields', async () => {
|
||||
expect(onSave).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
async function openDropdownAndAddFilter(
|
||||
getByTestId: (id: string) => HTMLElement,
|
||||
findByRole: (role: string, opts: { name: RegExp }) => Promise<HTMLElement>,
|
||||
) {
|
||||
fireEvent.mouseEnter(getByTestId('new-item-dropdown-button'));
|
||||
fireEvent.click(await findByRole('menuitem', { name: /add filter/i }));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('createNewOnOpen', () => {
|
||||
test('does not show alert when there is no unsaved filters', async () => {
|
||||
@@ -99,15 +107,23 @@ describe('createNewOnOpen', () => {
|
||||
onCancel,
|
||||
createNewOnOpen: false,
|
||||
});
|
||||
const dropdownButton = getByTestId('new-item-dropdown-button');
|
||||
fireEvent.mouseEnter(dropdownButton);
|
||||
const addFilterMenuItem = await findByRole('menuitem', {
|
||||
name: /add filter/i,
|
||||
});
|
||||
fireEvent.click(addFilterMenuItem);
|
||||
await openDropdownAndAddFilter(getByTestId, findByRole);
|
||||
fireEvent.click(getByRole('button', { name: 'Cancel' }));
|
||||
expect(onCancel).toHaveBeenCalledTimes(0);
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
expect(getByRole('alert')).toHaveTextContent('There are unsaved changes.');
|
||||
});
|
||||
|
||||
test('confirm-cancel button proceeds with cancel after the unsaved alert', async () => {
|
||||
const onCancel = jest.fn();
|
||||
const { getByRole, getByTestId, findByRole } = setup({
|
||||
onCancel,
|
||||
createNewOnOpen: false,
|
||||
});
|
||||
await openDropdownAndAddFilter(getByTestId, findByRole);
|
||||
fireEvent.click(getByRole('button', { name: 'Cancel' }));
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('native-filter-modal-confirm-cancel-button'));
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
88
superset-frontend/src/extensions/ExtensionsList.test.tsx
Normal file
88
superset-frontend/src/extensions/ExtensionsList.test.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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 { render, waitFor } from 'spec/helpers/testing-library';
|
||||
import ExtensionsList from './ExtensionsList';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
beforeAll(() => fetchMock.unmockGlobal());
|
||||
|
||||
// Mock initial state for the store
|
||||
const mockInitialState = {
|
||||
extensions: {
|
||||
loading: false,
|
||||
resourceCount: 2,
|
||||
resourceCollection: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Test Extension 1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Test Extension 2',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
bulkSelectEnabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
addDangerToast: jest.fn(),
|
||||
addSuccessToast: jest.fn(),
|
||||
};
|
||||
|
||||
const renderWithStore = (props = {}) =>
|
||||
render(<ExtensionsList {...defaultProps} {...props} />, {
|
||||
useRedux: true,
|
||||
useQueryParams: true,
|
||||
useRouter: true,
|
||||
useTheme: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
test('renders extensions list with basic structure', async () => {
|
||||
renderWithStore();
|
||||
|
||||
// Check that the component renders
|
||||
expect(document.body).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays extension names in the list', async () => {
|
||||
renderWithStore();
|
||||
|
||||
await waitFor(() => {
|
||||
// These texts should appear somewhere in the rendered component
|
||||
expect(document.body).toHaveTextContent(/Extensions/);
|
||||
});
|
||||
});
|
||||
|
||||
test('calls toast functions when provided', () => {
|
||||
const addDangerToast = jest.fn();
|
||||
const addSuccessToast = jest.fn();
|
||||
|
||||
renderWithStore({
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
});
|
||||
|
||||
// The component should accept these props without error
|
||||
expect(addDangerToast).toBeDefined();
|
||||
expect(addSuccessToast).toBeDefined();
|
||||
});
|
||||
95
superset-frontend/src/extensions/ExtensionsList.tsx
Normal file
95
superset-frontend/src/extensions/ExtensionsList.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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 { t } from '@apache-superset/core/translation';
|
||||
import { FunctionComponent, useMemo } from 'react';
|
||||
import { useListViewResource } from 'src/views/CRUD/hooks';
|
||||
import { ListView } from 'src/components';
|
||||
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
type Extension = {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
interface ExtensionsListProps {
|
||||
addDangerToast: (msg: string) => void;
|
||||
addSuccessToast: (msg: string) => void;
|
||||
}
|
||||
|
||||
const ExtensionsList: FunctionComponent<ExtensionsListProps> = ({
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
}) => {
|
||||
const {
|
||||
state: { loading, resourceCount, resourceCollection },
|
||||
fetchData,
|
||||
refreshData,
|
||||
} = useListViewResource<Extension>(
|
||||
'extensions',
|
||||
t('Extensions'),
|
||||
addDangerToast,
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: t('Name'),
|
||||
accessor: 'name',
|
||||
size: 'lg',
|
||||
id: 'name',
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { name },
|
||||
},
|
||||
}: any) => name,
|
||||
},
|
||||
],
|
||||
[loading], // We need to monitor loading to avoid stale state in actions
|
||||
);
|
||||
|
||||
const menuData: SubMenuProps = {
|
||||
activeChild: 'Extensions',
|
||||
name: t('Extensions'),
|
||||
buttons: [],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubMenu {...menuData} />
|
||||
<ListView<Extension>
|
||||
columns={columns}
|
||||
count={resourceCount}
|
||||
data={resourceCollection}
|
||||
initialSort={[{ id: 'name', desc: false }]}
|
||||
pageSize={PAGE_SIZE}
|
||||
fetchData={fetchData}
|
||||
loading={loading}
|
||||
addDangerToast={addDangerToast}
|
||||
addSuccessToast={addSuccessToast}
|
||||
refreshData={refreshData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withToasts(ExtensionsList);
|
||||
@@ -29,20 +29,15 @@ function createMockExtension(overrides: Partial<Extension> = {}): Extension {
|
||||
name: 'Test Extension',
|
||||
description: 'A test extension',
|
||||
version: '1.0.0',
|
||||
dependencies: [],
|
||||
remoteEntry: '',
|
||||
extensionDependencies: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(ExtensionsLoader as any).instance = undefined;
|
||||
// Minimal host registry surface the loader wraps during module evaluation.
|
||||
(window as any).superset = {
|
||||
commands: { registerCommand: jest.fn() },
|
||||
menus: { registerMenuItem: jest.fn() },
|
||||
editors: { registerEditor: jest.fn() },
|
||||
views: { registerView: jest.fn() },
|
||||
};
|
||||
});
|
||||
|
||||
test('creates a singleton instance', () => {
|
||||
@@ -147,59 +142,3 @@ test('logs error when initializeExtensions fails', async () => {
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
/**
|
||||
* Stubs the module-federation machinery `loadModule` depends on so a fake
|
||||
* extension entry module (its `./index` factory) can be loaded in jsdom.
|
||||
* Returns a cleanup function that restores the patched globals.
|
||||
*/
|
||||
function mockRemoteModule(containerName: string, factory: () => unknown) {
|
||||
const appendChildSpy = jest
|
||||
.spyOn(document.head, 'appendChild')
|
||||
.mockImplementation((element: Node) => {
|
||||
if (element instanceof HTMLScriptElement && element.onload) {
|
||||
setTimeout(() => (element.onload as any)(new Event('load')), 0);
|
||||
}
|
||||
return element;
|
||||
});
|
||||
|
||||
(global as any).__webpack_init_sharing__ = jest
|
||||
.fn()
|
||||
.mockResolvedValue(undefined);
|
||||
(global as any).__webpack_share_scopes__ = { default: {} };
|
||||
(window as any)[containerName] = {
|
||||
init: jest.fn().mockResolvedValue(undefined),
|
||||
get: jest.fn().mockResolvedValue(factory),
|
||||
};
|
||||
|
||||
return () => {
|
||||
appendChildSpy.mockRestore();
|
||||
delete (global as any).__webpack_init_sharing__;
|
||||
delete (global as any).__webpack_share_scopes__;
|
||||
delete (window as any)[containerName];
|
||||
};
|
||||
}
|
||||
|
||||
const remoteExtension = (overrides: Partial<Extension> = {}) =>
|
||||
createMockExtension({
|
||||
id: 'remote-ext',
|
||||
remoteEntry: 'http://example/remoteEntry.js',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test('runs activate(context) hook for modern-style extensions', async () => {
|
||||
const loader = ExtensionsLoader.getInstance();
|
||||
const activate = jest.fn().mockResolvedValue(undefined);
|
||||
const factory = () => ({ activate });
|
||||
const cleanup = mockRemoteModule('remote-ext', factory);
|
||||
|
||||
await loader.initializeExtension(remoteExtension());
|
||||
|
||||
expect(activate).toHaveBeenCalledTimes(1);
|
||||
// The context object passed to activate must have a subscriptions array.
|
||||
expect(activate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ subscriptions: expect.any(Array) }),
|
||||
);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
@@ -17,17 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { logging } from '@apache-superset/core/utils';
|
||||
import type { common as core } from '@apache-superset/core';
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
import { store } from 'src/views/store';
|
||||
// Side-effect import: brings the `window.superset` global augmentation into scope.
|
||||
import 'src/extensions/supersetGlobal';
|
||||
|
||||
type Extension = core.Extension;
|
||||
type ExtensionContext = core.ExtensionContext;
|
||||
type ExtensionModule = core.ExtensionModule;
|
||||
|
||||
/**
|
||||
* Loads extension modules via webpack module federation.
|
||||
@@ -88,8 +81,7 @@ class ExtensionsLoader {
|
||||
|
||||
/**
|
||||
* Initializes a single extension.
|
||||
* If the extension has a remote entry, loads the module and runs its
|
||||
* `activate(context)` hook (or, for legacy extensions, its top-level
|
||||
* If the extension has a remote entry, loads the module (which triggers
|
||||
* side-effect registrations for commands, views, menus, and editors).
|
||||
* @param extension The extension to initialize.
|
||||
*/
|
||||
@@ -104,15 +96,12 @@ class ExtensionsLoader {
|
||||
`Failed to initialize extension ${extension.name}\n`,
|
||||
error,
|
||||
);
|
||||
store.dispatch(
|
||||
addDangerToast(t('Extension "%s" failed to load.', extension.name)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a single extension module via webpack module federation and runs its
|
||||
* `activate(context)` hook.
|
||||
* Loads a single extension module via webpack module federation.
|
||||
* The module's top-level side effects fire contribution registrations.
|
||||
* @param extension The extension to load.
|
||||
*/
|
||||
private async loadModule(extension: Extension): Promise<void> {
|
||||
@@ -160,21 +149,8 @@ class ExtensionsLoader {
|
||||
await container.init(__webpack_share_scopes__.default);
|
||||
|
||||
const factory = await container.get('./index');
|
||||
|
||||
// `context.subscriptions` is provided for extensions to push their
|
||||
// Disposables into. The host does not dispose them (lifecycle management is
|
||||
// deferred); extensions own the array for as long as they are active.
|
||||
const context: ExtensionContext = { subscriptions: [] };
|
||||
|
||||
// Evaluate the module factory. Extensions may register contributions as
|
||||
// top-level side effects here, or return a module exposing `activate`.
|
||||
const module = factory() as ExtensionModule | undefined;
|
||||
|
||||
// Preferred path: hand the extension its context so it can track every
|
||||
// registration it makes, synchronous or asynchronous.
|
||||
if (typeof module?.activate === 'function') {
|
||||
await module.activate(context);
|
||||
}
|
||||
// Execute the module factory - side effects fire registrations
|
||||
factory();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -72,7 +72,6 @@ afterEach(() => {
|
||||
test('renders without crashing', () => {
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
@@ -89,7 +88,6 @@ test('sets up global superset object when user is logged in', async () => {
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
@@ -111,7 +109,6 @@ test('sets up global superset object when user is logged in', async () => {
|
||||
test('does not set up global superset object when user is not logged in', async () => {
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: mockInitialStateNoUser,
|
||||
});
|
||||
|
||||
@@ -130,7 +127,6 @@ test('initializes ExtensionsLoader when user is logged in', async () => {
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
@@ -148,7 +144,6 @@ test('initializes ExtensionsLoader when user is logged in', async () => {
|
||||
test('does not initialize ExtensionsLoader when user is not logged in', async () => {
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: mockInitialStateNoUser,
|
||||
});
|
||||
|
||||
@@ -174,7 +169,6 @@ test('only initializes once even with multiple renders', async () => {
|
||||
|
||||
const { rerender } = render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
@@ -211,7 +205,6 @@ test('initializes ExtensionsLoader when EnableExtensions feature flag is enabled
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
@@ -241,7 +234,6 @@ test('does not initialize ExtensionsLoader when EnableExtensions feature flag is
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
|
||||
@@ -276,7 +268,6 @@ test('continues rendering children even when ExtensionsLoader initialization fai
|
||||
</ExtensionsStartup>,
|
||||
{
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: mockInitialState,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -16,66 +16,48 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { logging } from '@apache-superset/core/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
import * as supersetCore from '@apache-superset/core';
|
||||
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
|
||||
import {
|
||||
authentication,
|
||||
core,
|
||||
commands,
|
||||
dashboard,
|
||||
dataset,
|
||||
editors,
|
||||
explore,
|
||||
extensions,
|
||||
menus,
|
||||
navigation,
|
||||
sqlLab,
|
||||
views,
|
||||
} from 'src/core';
|
||||
import { notifyPageChange } from 'src/core/navigation';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from 'src/views/store';
|
||||
import ExtensionsLoader from './ExtensionsLoader';
|
||||
// Side-effect import: brings the `window.superset` global augmentation into scope.
|
||||
import 'src/extensions/supersetGlobal';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
superset: {
|
||||
authentication: typeof authentication;
|
||||
core: typeof core;
|
||||
commands: typeof commands;
|
||||
editors: typeof editors;
|
||||
extensions: typeof extensions;
|
||||
menus: typeof menus;
|
||||
sqlLab: typeof sqlLab;
|
||||
views: typeof views;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const location = useLocation();
|
||||
const prevPathname = useRef<string | null>(null);
|
||||
|
||||
const userId = useSelector<RootState, number | undefined>(
|
||||
({ user }) => user.userId,
|
||||
);
|
||||
|
||||
// Notify the navigation namespace on every route change.
|
||||
useEffect(() => {
|
||||
if (prevPathname.current !== location.pathname) {
|
||||
prevPathname.current = location.pathname;
|
||||
notifyPageChange(location.pathname);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
// Log unhandled rejections that may originate from extension code.
|
||||
// Registered once for the lifetime of the app; does not suppress the
|
||||
// browser's default error surfacing so host error reporting is unaffected.
|
||||
useEffect(() => {
|
||||
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
|
||||
logging.error('[extensions] Unhandled rejection:', event.reason);
|
||||
};
|
||||
window.addEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
'unhandledrejection',
|
||||
handleUnhandledRejection,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialized) return;
|
||||
|
||||
@@ -85,33 +67,27 @@ const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Provide the implementations for @apache-superset/core.
|
||||
// Namespaces are listed explicitly — do not spread the core package here,
|
||||
// as that would leak un-contracted symbols onto window.superset.
|
||||
// Provide the implementations for @apache-superset/core
|
||||
window.superset = {
|
||||
...supersetCore,
|
||||
authentication,
|
||||
core,
|
||||
commands,
|
||||
dashboard,
|
||||
dataset,
|
||||
editors,
|
||||
explore,
|
||||
extensions,
|
||||
menus,
|
||||
navigation,
|
||||
sqlLab,
|
||||
views,
|
||||
};
|
||||
|
||||
// Render the host immediately; extension bundles load in the background.
|
||||
// ChatbotMount re-resolves reactively once the chatbot extension registers
|
||||
// (via subscribeToRegistry / getRegistryVersion), so the bubble appears
|
||||
// without blocking the UI.
|
||||
setInitialized(true);
|
||||
const setup = async () => {
|
||||
if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
|
||||
await ExtensionsLoader.getInstance().initializeExtensions();
|
||||
}
|
||||
setInitialized(true);
|
||||
};
|
||||
|
||||
if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
|
||||
ExtensionsLoader.getInstance().initializeExtensions();
|
||||
}
|
||||
setup();
|
||||
}, [initialized, userId]);
|
||||
|
||||
if (!initialized) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user