Compare commits

..
Author SHA1 Message Date
Mehmet Salih Yavuz 4c233d92a3 feat(frontend): migrate react-router v5 to TanStack Router with intent-based preloading
- Replace BrowserRouter/Switch/Route with a TanStack route tree built
  from the existing routes.tsx table; route chunks preload on link
  hover/focus (defaultPreload: 'intent') via fire-and-forget loaders
- Custom raw-string search param codec so rison payloads round-trip
  through the router untouched
- TanstackRouterAdapter replaces ReactRouter5Adapter for use-query-params
- StandaloneRouter (RouterContextProvider-based, synchronous mount) hosts
  the menu entry on Flask pages and component tests
- pushAppHref/replaceAppHref helpers restore v5's basename-prefixing
  semantics for raw href navigation
- useUnsavedChangesPrompt now uses useBlocker
- Migrate all 92 importing files; drop react-router-dom dependency
2026-06-10 23:25:23 +03:00
516 changed files with 13764 additions and 41874 deletions
+1 -1
View File
@@ -41,8 +41,8 @@ body:
label: Superset version
options:
- master / latest-dev
- "6.1.0"
- "6.0.0"
- "5.0.0"
validations:
required: true
- type: dropdown
+1 -1
View File
@@ -42,7 +42,7 @@ runs:
fi
echo "python-version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Python ${{ steps.set-python-version.outputs.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ steps.set-python-version.outputs.python-version }}
cache: ${{ inputs.cache }}
+4
View File
@@ -3,6 +3,10 @@ enable-beta-ecosystems: true
updates:
- package-ecosystem: "github-actions"
directory: "/"
ignore:
# Ignore temporarily as release schedule is too mentally taxing for dep-handling maintainers
# Additionally, very few PRs are reviewed by this action.
- dependency-name: anthropics/claude-code-action
schedule:
interval: "daily"
cooldown:
+88
View File
@@ -0,0 +1,88 @@
name: Claude PR Assistant
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
permissions:
contents: read
jobs:
check-permissions:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))
runs-on: ubuntu-latest
outputs:
allowed: ${{ steps.check.outputs.allowed }}
steps:
- name: Check if user is allowed
id: check
env:
COMMENTER: ${{ github.event.comment.user.login }}
run: |
# List of allowed users
ALLOWED_USERS="mistercrunch,rusackas"
echo "Checking permissions for user: $COMMENTER"
# Check if user is in allowed list
if [[ ",$ALLOWED_USERS," == *",$COMMENTER,"* ]]; then
echo "allowed=true" >> $GITHUB_OUTPUT
echo "✅ User $COMMENTER is allowed to use Claude"
else
echo "allowed=false" >> $GITHUB_OUTPUT
echo "❌ User $COMMENTER is not allowed to use Claude"
fi
deny-access:
needs: check-permissions
if: needs.check-permissions.outputs.allowed == 'false'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Comment access denied
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
COMMENTER_LOGIN: ${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}
with:
script: |
const commenter = process.env.COMMENTER_LOGIN;
const message = `👋 Hi @${commenter}!
Thanks for trying to use Claude Code, but currently only certain team members have access to this feature.
If you believe you should have access, please contact a project maintainer.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
claude-code-action:
needs: check-permissions
if: needs.check-permissions.outputs.allowed == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
steps:
- 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"
+2 -2
View File
@@ -63,7 +63,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # 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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
with:
category: "/language:${{matrix.language}}"
+7 -54
View File
@@ -75,24 +75,6 @@ jobs:
with:
persist-credentials: false
- name: Free up disk space
shell: bash
run: |
# Reclaim large preinstalled toolchains we don't use. The image
# build, and especially the docker-compose sanity check (which
# rebuilds from scratch whenever the registry cache image
# apache/superset-cache is unavailable), can otherwise exhaust the
# runner's root disk and fail with "no space left on device".
echo "Disk before cleanup:"; df -h /
sudo rm -rf \
/usr/share/dotnet \
/usr/local/lib/android \
/opt/ghc \
/usr/local/.ghcup \
/opt/hostedtoolcache/CodeQL \
/usr/local/share/boost || true
echo "Disk after cleanup:"; df -h /
- name: Setup Docker Environment
uses: ./.github/actions/setup-docker
with:
@@ -119,27 +101,13 @@ jobs:
PUSH_OR_LOAD="--load"
fi
# Retry to absorb transient Docker Hub registry errors (base-image
# pull timeouts, 504/401 on push, ECONNRESET) that otherwise fail
# the whole job. buildx reuses the buildkit layer cache from the
# failed attempt, so a retry mostly re-does just the failed push.
for attempt in 1 2 3; do
if supersetbot docker \
$PUSH_OR_LOAD \
--preset "$BUILD_PRESET" \
--context "$EVENT" \
--context-ref "$RELEASE" $FORCE_LATEST \
--extra-flags "--build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
$PLATFORM_ARG; then
break
fi
if [ "$attempt" -eq 3 ]; then
echo "::error::supersetbot docker build failed after 3 attempts"
exit 1
fi
echo "::warning::Build attempt ${attempt} failed; retrying in 30s..."
sleep 30
done
supersetbot docker \
$PUSH_OR_LOAD \
--preset "$BUILD_PRESET" \
--context "$EVENT" \
--context-ref "$RELEASE" $FORCE_LATEST \
--extra-flags "--build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
$PLATFORM_ARG
# in the context of push (using multi-platform build), we need to pull the image locally
- name: Docker pull
@@ -180,21 +148,6 @@ jobs:
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Free up disk space
shell: bash
run: |
# The sanity check rebuilds the image from scratch whenever the
# registry cache image apache/superset-cache is unavailable, which
# can exhaust the runner's root disk ("no space left on device").
echo "Disk before cleanup:"; df -h /
sudo rm -rf \
/usr/share/dotnet \
/usr/local/lib/android \
/opt/ghc \
/usr/local/.ghcup \
/opt/hostedtoolcache/CodeQL \
/usr/local/share/boost || true
echo "Disk after cleanup:"; df -h /
- name: Setup Docker Environment
uses: ./.github/actions/setup-docker
with:
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: "superset-frontend/.nvmrc"
node-version: "20"
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
@@ -56,7 +56,7 @@ jobs:
- name: Upload coverage reports to Codecov
if: steps.check.outputs.superset-extensions-cli
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
file: ./coverage.xml
flags: superset-extensions-cli
+1 -1
View File
@@ -134,7 +134,7 @@ jobs:
run: npx nyc merge coverage/ merged-output/coverage-summary.json
- name: Upload Code Coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: javascript
use_oidc: true
@@ -85,7 +85,7 @@ jobs:
run: |
./scripts/python_tests.sh
- name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,mysql
verbose: true
@@ -173,7 +173,7 @@ jobs:
run: |
./scripts/python_tests.sh
- name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,postgres
verbose: true
@@ -222,7 +222,7 @@ jobs:
run: |
./scripts/python_tests.sh
- name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,sqlite
verbose: true
@@ -90,7 +90,7 @@ jobs:
run: |
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
- name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,presto
verbose: true
@@ -152,7 +152,7 @@ jobs:
pip install -e .[hive]
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
- name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,hive
verbose: true
@@ -72,7 +72,7 @@ jobs:
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
- name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,unit
verbose: true
+1 -1
View File
@@ -29,7 +29,7 @@ ARG BUILD_TRANSLATIONS="false"
######################################################################
# superset-node-ci used as a base for building frontend assets and CI
######################################################################
FROM --platform=${BUILDPLATFORM} node:24-trixie-slim AS superset-node-ci
FROM --platform=${BUILDPLATFORM} node:22-trixie-slim AS superset-node-ci
ARG BUILD_TRANSLATIONS
ENV BUILD_TRANSLATIONS=${BUILD_TRANSLATIONS}
ARG DEV_MODE="false" # Skip frontend build in dev mode
-105
View File
@@ -24,63 +24,6 @@ assists people when migrating to a new version.
## Next
### `thumbnail_url` removed from dashboard list API response
The `thumbnail_url` field has been removed from `GET /api/v1/dashboard/` list responses. External consumers relying on this field must now construct the thumbnail URL client-side using `id` and `changed_on_utc`:
```
/api/v1/dashboard/{id}/thumbnail/{changed_on_utc}/
```
The thumbnail endpoint redirects to the current digest URL regardless of whether the supplied digest is exact. If the image is not yet cached, that digest URL may return `202` and trigger async generation. Using `changed_on_utc` as the digest is sufficient for cache-busting purposes.
### Webhook alerts/reports block private/internal hosts by default
Webhook alert/report dispatch (`WebhookNotification.send`) now validates the target URL's host against the same private/internal-IP block applied to dataset import URLs. If the resolved host is in a loopback, link-local, private (RFC-1918), shared-CGNAT, or multicast range, the webhook is rejected with `NotificationParamException`.
Deployments that intentionally point webhooks at internal targets (chatops bridges, internal automation servers, on-premises Mattermost/Rocket.Chat, etc.) can opt out by setting `ALERT_REPORTS_WEBHOOK_ALLOW_INTERNAL_HOSTS = True` in `superset_config.py`. This mirrors the existing `DATASET_IMPORT_ALLOW_INTERNAL_DATA_URLS` opt-out for dataset imports.
### Impala cancel_query blocks private/internal hosts by default
The Impala engine spec's `cancel_query` issues an HTTP request from the Superset backend to the host configured on the Impala database connection. That host is now validated before the request: if it resolves to a private/internal IP range, the cancel call is refused and a warning is logged. Operators whose Impala cluster runs on an internal network can opt out by setting `IMPALA_CANCEL_QUERY_ALLOW_INTERNAL_HOSTS = True` in `superset_config.py`. This mirrors the dataset-import and webhook opt-out flags.
### Map chart renderer and OpenStreetMap migration behavior
The MapLibre migration for deck.gl charts preserves saved non-Mapbox styles on
the MapLibre-compatible path. Saved styles such as OpenStreetMap, `tile://`
tile templates, generic HTTPS style URLs, and charts without a saved style are
not reclassified as Mapbox during migration and do not require
`MAPBOX_API_KEY` only because of the migration.
Saved true Mapbox styles whose value starts with `mapbox://` remain
Mapbox-backed. If a Superset deployment does not configure `MAPBOX_API_KEY`,
those saved Mapbox charts keep the existing missing-key message instead of
silently falling back to MapLibre or another provider. In Explore, deck.gl and
point-cluster renderer controls preserve saved Mapbox state, but the Mapbox
choice is not available as a new working renderer without a configured key.
The MapLibre style choices include `Streets (OSM)`, backed by
`https://tile.openstreetmap.org/{z}/{x}/{y}.png`. This OpenStreetMap tile
service requires visible `© OpenStreetMap contributors` attribution and should
be used through normal browser map tile requests and caching; it is not intended
for bulk prefetch or offline tile downloads.
### Password complexity policy enabled by default
Superset now ships a default password-complexity policy, enforced (via Flask-AppBuilder) across self-registration, the user create/edit/reset forms, and the User REST API. The policy requires a minimum password length of 8 characters and rejects a built-in blocklist of common/guessable passwords.
This is enabled by default (`FAB_PASSWORD_COMPLEXITY_ENABLED = True`), so new or reset passwords that are too short or appear in the blocklist will be rejected where they were previously accepted. Existing stored passwords are unaffected until they are next changed.
Operators can tune or disable the policy via config:
- `AUTH_PASSWORD_MIN_LENGTH` — minimum length (default `8`).
- `AUTH_PASSWORD_COMMON_BLOCKLIST` — extra passwords to reject, in addition to the built-in list.
- `FAB_PASSWORD_COMPLEXITY_VALIDATOR` — replace with your own callable for custom rules.
- `FAB_PASSWORD_COMPLEXITY_ENABLED = False` — disable enforcement entirely.
### Data uploads bounded by UPLOAD_MAX_FILE_SIZE_BYTES
Single data-file uploads (CSV, Excel, columnar) are now bounded by the `UPLOAD_MAX_FILE_SIZE_BYTES` config option, which defaults to `100 * 1024 * 1024` (100 MB). Files larger than this are rejected with a `413` before their contents are buffered into memory. Set `UPLOAD_MAX_FILE_SIZE_BYTES = None` to disable the check and restore unbounded uploads.
### Duration formatter precision
The `DURATION` number formatter now uses `Intl.DurationFormat` for locale-aware output. By default, sub-second fields are omitted, so values that previously displayed fractional seconds with `pretty-ms`, such as `10500` milliseconds rendering as `10.5s`, now render as `10s`.
@@ -127,24 +70,6 @@ superset revoke-guest-tokens
This change is backward compatible. The feature is off by default, and even when enabled nothing is revoked until an admin explicitly bumps the version: the expected version starts at `0`, and tokens minted before this change (which carry no version claim) are treated as version `0`. No database migration is required.
### Sessions are terminated when an account is disabled
Disabling a user account (setting `active` to `False`, via the admin UI, REST API, or CLI) now terminates that user's outstanding sessions on their next request, instead of relying on a passive check. This works for both client-side cookie sessions and server-side session stores via a per-user invalidation epoch (`user_attribute.sessions_invalidated_at`, added by a migration). The mechanism is inert for users that were never disabled (NULL epoch), so there is no behavior change for active users. Re-enabling an account and logging in again starts a fresh, valid session. The migration backfills the epoch for accounts that are already disabled at upgrade time, so re-enabling such an account does not revive a session that predates this feature.
### Opt-in SSH tunnel server host key verification
SSH tunnels can now optionally pin the expected SSH server host key as a defense-in-depth measure against man-in-the-middle attacks. paramiko's transport performs no known-hosts checking by default, so previously the SSH server's identity was not verified. This feature is opt-in and off by default; existing tunnels are unaffected.
- A new nullable `server_host_key` column on the `ssh_tunnels` table stores the expected host key in authorized-key form (e.g. `ssh-ed25519 AAAA...`). It is a public key and is stored in plaintext. It can be set via the SSH tunnel POST/PUT payloads (`ssh_tunnel.server_host_key`).
- When a tunnel has `server_host_key` set, Superset connects to the SSH server, reads the host key it presents, and rejects the tunnel if it does not match.
- A new config flag `SSH_TUNNEL_STRICT_HOST_KEY_CHECKING` (default `False`) controls fail-closed behavior. When `True`, every tunnel must declare a `server_host_key`; a tunnel without one is rejected.
Runbook to adopt:
1. Capture the SSH server's host key, e.g. `ssh-keyscan -t ed25519 ssh.example.com` (verify it out-of-band).
2. Set that value on the tunnel's `server_host_key` (via the database/SSH tunnel API or UI payload).
3. Optionally set `SSH_TUNNEL_STRICT_HOST_KEY_CHECKING = True` in `superset_config.py` to require host-key verification on all tunnels.
### 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.
@@ -164,36 +89,6 @@ Both default to empty (no behavior change). They apply to both the `LOCAL_EXTENS
The Dynamic Group By chart customization now orders its display values according to the "Sort display control values" toggle: ascending (AZ), descending (ZA), or the dataset's source order when the toggle is unset. Previously the dropdown always sorted alphabetically. Existing dashboards where the toggle was never set will show options in source order instead of AZ; open the customization and enable the toggle to restore alphabetical ordering.
### Selectable encryption engine for app-encrypted fields (AES-GCM)
App-encrypted fields (database passwords, SSH tunnel credentials, OAuth tokens, etc.) can now use authenticated **AES-GCM** encryption instead of the historical unauthenticated **AES-CBC**. A new config selects the engine for the default adapter:
```python
# "aes" (AES-CBC, historical default) | "aes-gcm" (authenticated, recommended for new installs)
SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes"
```
**No action required / no behavior change:** the default remains `"aes"`, so existing installs are unaffected.
**Opting in on an existing install:** flipping the engine on a populated database without re-encrypting first will make stored secrets undecryptable, because the two ciphertext formats are not compatible. A migrator is provided. Recommended runbook:
1. Take a metadata-DB backup.
2. Re-encrypt existing secrets into the new engine (the `SECRET_KEY` is unchanged):
```bash
superset re-encrypt-secrets --engine aes-gcm
```
3. Set `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes-gcm"` in your config.
4. Restart Superset.
5. Re-run the migrator once more after the restart:
```bash
superset re-encrypt-secrets --engine aes-gcm
```
A live instance keeps writing *new* secrets as AES-CBC during the window between step 2 and the restart in step 4; this second pass sweeps those up (it is idempotent, so already-migrated values are skipped).
Schedule the cutover in a quiet window. Runtime reads use only the single configured engine, so in a multi-worker deployment there is an unavoidable brief decrypt-outage between the migration commit and the last worker restarting with the new config — each migrator run is transactional, but the fleet-wide cutover is not zero-downtime.
The migration is transactional (all-or-nothing) and idempotent — it can be safely re-run or resumed. Note that AES-GCM, unlike AES-CBC, does not support querying directly over encrypted columns; audit any code that filters on an encrypted column before switching. See the SIP at `docs/sip/authenticated-encryption-at-rest.md` for details.
### Granular Export Controls
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
+2 -2
View File
@@ -71,12 +71,12 @@ case "${1}" in
worker)
echo "Starting Celery worker..."
# setting up only 2 workers by default to contain memory usage in dev environments
celery --app=superset.tasks.celery_app:app worker -O fair -l INFO --concurrency=${CELERYD_CONCURRENCY:-2} ${WORKER_LOG_FILE:+--logfile=$WORKER_LOG_FILE}
celery --app=superset.tasks.celery_app:app worker -O fair -l INFO --concurrency=${CELERYD_CONCURRENCY:-2}
;;
beat)
echo "Starting Celery beat..."
rm -f /tmp/celerybeat.pid
celery --app=superset.tasks.celery_app:app beat --pidfile /tmp/celerybeat.pid -l INFO -s "${SUPERSET_HOME}"/celerybeat-schedule ${BEAT_LOG_FILE:+--logfile=$BEAT_LOG_FILE}
celery --app=superset.tasks.celery_app:app beat --pidfile /tmp/celerybeat.pid -l INFO -s "${SUPERSET_HOME}"/celerybeat-schedule
;;
app)
echo "Starting web app (using development server)..."
+1 -1
View File
@@ -1 +1 @@
v24.16.0
v22.22.0
+6 -6
View File
@@ -70,10 +70,10 @@
"@storybook/preview-api": "^8.6.18",
"@storybook/theming": "^8.6.15",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.41",
"@swc/core": "^1.15.40",
"antd": "^6.4.3",
"baseline-browser-mapping": "^2.10.35",
"caniuse-lite": "^1.0.30001797",
"baseline-browser-mapping": "^2.10.33",
"caniuse-lite": "^1.0.30001793",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
"js-yaml": "^4.2.0",
@@ -101,15 +101,15 @@
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.61.0",
"@typescript-eslint/parser": "^8.60.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.6.0",
"prettier": "^3.8.4",
"prettier": "^3.8.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.61.0",
"typescript-eslint": "^8.60.1",
"webpack": "^5.107.2"
},
"browserslist": {
@@ -1,136 +0,0 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# SIP: Authenticated encryption (AES-GCM) for app-encrypted fields
## [DRAFT — proposal for discussion]
This document is a draft proposal accompanying the code in this PR. It is
intended to seed the formal SIP discussion. The code here ships the
backward-compatible engine selection **and** the re-encryption migrator
(Phases 12 below); both are opt-in and change nothing for existing installs by
default. Flipping the default for fresh installs (Phase 3) remains future work.
## Motivation
Superset app-encrypts a number of sensitive fields before persisting them to
the metadata database, including:
- database connection passwords and `encrypted_extra` (`superset/models/core.py`),
- SSH tunnel credentials — password, private key, private-key password
(`superset/databases/ssh_tunnel/models.py`),
- OAuth2 tokens and other secrets stored via `EncryptedType`.
These fields are encrypted with `sqlalchemy_utils.EncryptedType`, which
**defaults to `AesEngine` (AES-CBC)**. AES-CBC provides confidentiality but is
**unauthenticated**: it has no integrity tag. An attacker with write access to
the ciphertext (e.g. direct metadata-DB access, a backup, or a compromised
replica) can perform **bit-flipping / chosen-ciphertext manipulation** to
silently alter the decrypted plaintext of a secret without detection.
`AesGcmEngine` (AES-GCM) is authenticated encryption: tampering causes
decryption to fail loudly rather than yielding attacker-influenced plaintext.
Using authenticated encryption for secrets at rest is an ASVS L1 expectation
(11.3.2 / cryptography best practice).
`config.py` already documents that operators *can* switch to GCM by writing a
custom `AbstractEncryptedFieldAdapter`, but:
1. it is opt-in, undocumented as a security recommendation, and easy to miss;
2. there is **no migration path** — flipping the engine on a populated database
makes every existing secret undecryptable, because GCM ciphertext is not
format-compatible with CBC.
## Proposed change
A three-part change, delivered incrementally so existing deployments are never
broken:
### Phase 1 — engine selection (this PR)
- Add a `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE` config (`"aes"` | `"aes-gcm"`),
**defaulting to `"aes"`** (no behavior change for existing installs).
- Teach the default `SQLAlchemyUtilsAdapter` to honor it (an explicit `engine`
kwarg still wins, so the migrator can pin an engine).
- This lets **new** deployments choose AES-GCM from day one with a one-line
config, instead of writing a custom adapter.
### Phase 2 — CBC→GCM re-encryption migrator (this PR)
The existing `SecretsMigrator` (previously only used for `SECRET_KEY` rotation)
gains an **engine migration** mode that:
1. discovers every `EncryptedType` column (via `discover_encrypted_fields()`),
2. decrypts each value with the **source** engine (AES-CBC) under the current
`SECRET_KEY`,
3. re-encrypts with the **target** engine (AES-GCM),
4. runs transactionally per the existing all-or-nothing semantics, and is
idempotent per column (already-migrated values are skipped), so a run can be
safely repeated or resumed.
Exposed via a new `--engine` option on the existing CLI command:
`superset re-encrypt-secrets --engine aes-gcm`, runnable by operators with a DB
backup in hand. The `SECRET_KEY` is unchanged; an engine change and a key
rotation can also be combined (pass `--previous_secret_key` as well).
### Phase 3 — flip the default for new installs
Once the migrator and docs are in place, change the default to `"aes-gcm"` for
**fresh** installs only (e.g. keyed off an empty metadata DB / documented in
`UPDATING.md`), keeping existing installs on `"aes"` until they run Phase 2.
## New or changed public interfaces
- New config: `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE: Literal["aes", "aes-gcm"]`.
- New (Phase 2) CLI: `superset re-encrypt-secrets --engine <name>`.
- No schema changes; ciphertext format changes per migrated column.
## Migration plan and compatibility
- **Backward compatible by default.** Phase 1 changes nothing unless the
operator opts in.
- Switching an existing deployment to `"aes-gcm"` **without** running the Phase
2 migrator will make existing secrets undecryptable — this is called out in
the config comment and must be in `UPDATING.md`.
- Recommended operator runbook: take a metadata-DB backup → run
`re-encrypt-secrets --engine aes-gcm` → set
`SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes-gcm"` → restart → re-run
`re-encrypt-secrets --engine aes-gcm` once more to sweep up any secrets a live
instance wrote as AES-CBC during the cutover window. The canonical, more
detailed version of this runbook lives in `UPDATING.md`; this is a summary.
- `AesEngine` allows queryability over encrypted fields; AES-GCM does not.
Any code that filters/queries on an encrypted column directly must be audited
before Phase 3 (none is expected, but it must be verified).
## Rejected alternatives
- **Flip the default immediately.** Rejected: bricks every existing
deployment's secrets with no migration path.
- **Document-only (custom adapter).** Status quo; high friction and no
migration tooling — most operators will never do it.
## Open questions
- GCM→CBC rollback (for operators who need queryability) already works via the
same command (`re-encrypt-secrets --engine aes`), since the migrator is
engine-symmetric. Should rollback be documented as a supported path or
discouraged?
- The migrator already supports a concurrent `SECRET_KEY` rotation + engine
change in a single pass (pass `--previous_secret_key` alongside `--engine`).
Is that combination worth calling out in the operator docs, or kept advanced?
+2 -2
View File
@@ -7235,10 +7235,10 @@
"pypi_packages": [
"oracledb"
],
"connection_string": "oracle+oracledb://{username}:{password}@{hostname}:{port}",
"connection_string": "oracle://{username}:{password}@{hostname}:{port}",
"default_port": 1521,
"notes": "Previously used cx_Oracle, now uses oracledb.",
"docs_url": "https://python-oracledb.readthedocs.io/en/latest/user_guide/installation.html",
"docs_url": "https://cx-oracle.readthedocs.io/en/latest/user_guide/installation.html",
"category": "Other Databases"
},
"engine": "oracle",
+181 -291
View File
@@ -265,15 +265,6 @@
js-tokens "^4.0.0"
picocolors "^1.1.1"
"@babel/code-frame@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7"
integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==
dependencies:
"@babel/helper-validator-identifier" "^7.29.7"
js-tokens "^4.0.0"
picocolors "^1.1.1"
"@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0":
version "7.28.0"
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz"
@@ -284,25 +275,20 @@
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz"
integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==
"@babel/compat-data@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629"
integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==
"@babel/core@^7.21.3", "@babel/core@^7.25.9":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7"
integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==
version "7.28.6"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz"
integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==
dependencies:
"@babel/code-frame" "^7.29.7"
"@babel/generator" "^7.29.7"
"@babel/helper-compilation-targets" "^7.29.7"
"@babel/helper-module-transforms" "^7.29.7"
"@babel/helpers" "^7.29.7"
"@babel/parser" "^7.29.7"
"@babel/template" "^7.29.7"
"@babel/traverse" "^7.29.7"
"@babel/types" "^7.29.7"
"@babel/code-frame" "^7.28.6"
"@babel/generator" "^7.28.6"
"@babel/helper-compilation-targets" "^7.28.6"
"@babel/helper-module-transforms" "^7.28.6"
"@babel/helpers" "^7.28.6"
"@babel/parser" "^7.28.6"
"@babel/template" "^7.28.6"
"@babel/traverse" "^7.28.6"
"@babel/types" "^7.28.6"
"@jridgewell/remapping" "^2.3.5"
convert-source-map "^2.0.0"
debug "^4.1.0"
@@ -332,17 +318,6 @@
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
"@babel/generator@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3"
integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==
dependencies:
"@babel/parser" "^7.29.7"
"@babel/types" "^7.29.7"
"@jridgewell/gen-mapping" "^0.3.12"
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3":
version "7.27.3"
resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz"
@@ -350,7 +325,7 @@
dependencies:
"@babel/types" "^7.27.3"
"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2":
"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2", "@babel/helper-compilation-targets@^7.28.6":
version "7.28.6"
resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz"
integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==
@@ -361,17 +336,6 @@
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-compilation-targets@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042"
integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==
dependencies:
"@babel/compat-data" "^7.29.7"
"@babel/helper-validator-option" "^7.29.7"
browserslist "^4.24.0"
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3":
version "7.28.3"
resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz"
@@ -410,11 +374,6 @@
resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz"
integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
"@babel/helper-globals@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b"
integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==
"@babel/helper-member-expression-to-functions@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz"
@@ -439,14 +398,6 @@
"@babel/traverse" "^7.28.6"
"@babel/types" "^7.28.6"
"@babel/helper-module-imports@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396"
integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==
dependencies:
"@babel/traverse" "^7.29.7"
"@babel/types" "^7.29.7"
"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.6":
version "7.28.6"
resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz"
@@ -456,15 +407,6 @@
"@babel/helper-validator-identifier" "^7.28.5"
"@babel/traverse" "^7.28.6"
"@babel/helper-module-transforms@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae"
integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==
dependencies:
"@babel/helper-module-imports" "^7.29.7"
"@babel/helper-validator-identifier" "^7.29.7"
"@babel/traverse" "^7.29.7"
"@babel/helper-optimise-call-expression@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz"
@@ -513,31 +455,16 @@
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-string-parser@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f"
integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==
"@babel/helper-validator-identifier@^7.28.5":
version "7.28.5"
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz"
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
"@babel/helper-validator-identifier@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2"
integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==
"@babel/helper-validator-option@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz"
integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==
"@babel/helper-validator-option@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a"
integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==
"@babel/helper-wrap-function@^7.27.1":
version "7.28.3"
resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz"
@@ -547,13 +474,13 @@
"@babel/traverse" "^7.28.3"
"@babel/types" "^7.28.2"
"@babel/helpers@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607"
integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==
"@babel/helpers@^7.28.6":
version "7.28.6"
resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz"
integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==
dependencies:
"@babel/template" "^7.29.7"
"@babel/types" "^7.29.7"
"@babel/template" "^7.28.6"
"@babel/types" "^7.28.6"
"@babel/parser@^7.28.6":
version "7.28.6"
@@ -569,13 +496,6 @@
dependencies:
"@babel/types" "^7.29.0"
"@babel/parser@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334"
integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==
dependencies:
"@babel/types" "^7.29.7"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz"
@@ -1252,15 +1172,6 @@
"@babel/parser" "^7.28.6"
"@babel/types" "^7.28.6"
"@babel/template@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700"
integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==
dependencies:
"@babel/code-frame" "^7.29.7"
"@babel/parser" "^7.29.7"
"@babel/types" "^7.29.7"
"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.6":
version "7.28.6"
resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz"
@@ -1287,19 +1198,6 @@
"@babel/types" "^7.29.0"
debug "^4.3.1"
"@babel/traverse@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d"
integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==
dependencies:
"@babel/code-frame" "^7.29.7"
"@babel/generator" "^7.29.7"
"@babel/helper-globals" "^7.29.7"
"@babel/parser" "^7.29.7"
"@babel/template" "^7.29.7"
"@babel/types" "^7.29.7"
debug "^4.3.1"
"@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.4.4":
version "7.28.6"
resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz"
@@ -1316,14 +1214,6 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@babel/types@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92"
integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==
dependencies:
"@babel/helper-string-parser" "^7.29.7"
"@babel/helper-validator-identifier" "^7.29.7"
"@braintree/sanitize-url@^7.1.1":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz#ca2035b0fefe956a8676ff0c69af73e605fcd81f"
@@ -4143,86 +4033,86 @@
dependencies:
apg-lite "^1.0.4"
"@swc/core-darwin-arm64@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.41.tgz#4fcbc9cbb9dfc9027d66e2b23b8d1d0315d164bd"
integrity sha512-kREh6J5paQFvP3i7f/4FbqRNOJREutVFVOkder4GVyCBQ39YmER55cW/y1NNjwrchzFqgYswFn0mMDCqbqKzrw==
"@swc/core-darwin-arm64@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz#b05d715b04c4fd47baf59288233da85a683cc0bc"
integrity sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==
"@swc/core-darwin-x64@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.41.tgz#726c60a893e2f1a07bee28f79b519b8e6489415b"
integrity sha512-N8B56ESFazZAWZyIkecADSPCwlLEinW7QLMEeotCpv4J7VXwfH+OLkmRL8o96UZ+1355fwHxDTS6/wK7yucvkA==
"@swc/core-darwin-x64@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz#3180daef5c1e47b435f8edd084509e0a5c0d883b"
integrity sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==
"@swc/core-linux-arm-gnueabihf@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.41.tgz#08930e8015ca2fadc729546d5bd4b758a3999dda"
integrity sha512-6XrId2fyle0mS5xxON8rU84mPd2Cq1kDJRj+4BnQKTd7u+2kSA6Ww+JkOP0iTNqOqt9OXhPOEAjBHAuonWcdCg==
"@swc/core-linux-arm-gnueabihf@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz#18fcd3c70e48fdfae07c9f18751b1409ce1e5e84"
integrity sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==
"@swc/core-linux-arm64-gnu@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.41.tgz#6c27490a4013647a09ff64cea1d6b1169394602f"
integrity sha512-ynLIarxlkVnqHn1D0fKOVht6mNU5ks6lrH+MY3kkS+XFaGGgDxFZVjWKJlkYTKm3RCvBTfA8Ng5fLufXheMRKQ==
"@swc/core-linux-arm64-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz#26304933922f2a8e3194770e404403fc25a19c89"
integrity sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==
"@swc/core-linux-arm64-musl@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.41.tgz#4cce52fbbbe78b1f99c2a4e3f9ad2629f6eae494"
integrity sha512-dXu/5vd4gh8symyhRF+4G7gOPkjmb4pONhh7sl+6GSiW0LOKZlfu5kXmyFbTz9smOT7jgr002qY9b1nujjXt2A==
"@swc/core-linux-arm64-musl@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz#3402dfba04ba7b8ea81f243e2f8fa2c336b54d03"
integrity sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==
"@swc/core-linux-ppc64-gnu@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.41.tgz#3d1fadd8d320e7250a6b2a2d9c0b0d4dac162f97"
integrity sha512-XGO6zVPXoPE0gf/XnI4jBbafNT13AYgoh6ns0JCSdOetI/kqVf0vhpz7NuNgAzZrMVCsmieqjPoTwViDgh4mOQ==
"@swc/core-linux-ppc64-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz#b3df9065cad352328c1eeef08a28fc9fe98785aa"
integrity sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==
"@swc/core-linux-s390x-gnu@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.41.tgz#6e4c54168d4a8d7852ef797437bd25e6fb5d7a50"
integrity sha512-0WUglRwyZtW+iMi7J3iFdrCxreZZIKf4egTwEQfIYRsqFax69A0OrFj+NIoFSE03xBT/IFRrg+S8K6f9Ky+4hA==
"@swc/core-linux-s390x-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz#58e5b601f641dde81b30626ef66a668701ec918f"
integrity sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==
"@swc/core-linux-x64-gnu@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.41.tgz#5f947698786e15e2f696e0c6b3afd25138bae86b"
integrity sha512-VxkuQK59c0tHm6uJZCUrS3cyA2JhGGfdU6e41SZz0x/JS+4Sm7C1mIc97In14vkZJopEt7yXA2TouCqZDSygEA==
"@swc/core-linux-x64-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz#cf057dce0c148c53f2d30152baaf60ea29e5d59c"
integrity sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==
"@swc/core-linux-x64-musl@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.41.tgz#f4a0910cb273e39bcc09d572a08f62a355a93628"
integrity sha512-/0qXIu1ZxggLuovLb22vFfKHq2AA4n6Whw5UwmVCHk4pkw7KWnPIQpMCEqUMPsNkFJig7PPp/TSYFu8ZEb2rtQ==
"@swc/core-linux-x64-musl@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz#21fb1a4d0193e9bbcd1469ecd36166d2e96e4006"
integrity sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==
"@swc/core-win32-arm64-msvc@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.41.tgz#a55334b1b7c23a962d4219f332b6422f3c3374e4"
integrity sha512-Y481sMNZM6rECh9VO4+y26N1lWEDAyxnBZskUf37fl90uHE946VHfmiVQWT0uMFOhyJJFovGTRuF4W82dwewUg==
"@swc/core-win32-arm64-msvc@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz#1dba23b2b0db86b3d6d65da2abd627cc607a1fbc"
integrity sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==
"@swc/core-win32-ia32-msvc@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.41.tgz#e1135f8d6857f6c48e4bfb6105568b37b3f88dc5"
integrity sha512-BAchBD5qeUzy3hiPSLJtaaoSm4blCLyYffOF1bGE4ETcV+OisqjUAwDQMJj++4bTpvMCDzwC+Bj3PmQyBCtscw==
"@swc/core-win32-ia32-msvc@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz#b2da1e33165d469467b1046a2189db468da488eb"
integrity sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==
"@swc/core-win32-x64-msvc@1.15.41":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.41.tgz#52d241e2bf4c6154675c0ad447b29cbdb0ccb547"
integrity sha512-WOkA+fJ/ViVBQDsSV9JC52NACTe5PhlurA6viASDZGb7HR3KS01ZG7RZ+Bg6SVQFIoq3gSbTsskQVe6EbHFAYw==
"@swc/core-win32-x64-msvc@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz#3563f7e8ce8708f5fda43eb8e0956ef11e0da320"
integrity sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==
"@swc/core@^1.15.41", "@swc/core@^1.7.39":
version "1.15.41"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.41.tgz#a212c5040abd1ffd2ad6caf140f0d586ffcfaa6e"
integrity sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ==
"@swc/core@^1.15.40", "@swc/core@^1.7.39":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.40.tgz#941c949aa88c0d8d291f102f519f3c2c77701b90"
integrity sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==
dependencies:
"@swc/counter" "^0.1.3"
"@swc/types" "^0.1.26"
optionalDependencies:
"@swc/core-darwin-arm64" "1.15.41"
"@swc/core-darwin-x64" "1.15.41"
"@swc/core-linux-arm-gnueabihf" "1.15.41"
"@swc/core-linux-arm64-gnu" "1.15.41"
"@swc/core-linux-arm64-musl" "1.15.41"
"@swc/core-linux-ppc64-gnu" "1.15.41"
"@swc/core-linux-s390x-gnu" "1.15.41"
"@swc/core-linux-x64-gnu" "1.15.41"
"@swc/core-linux-x64-musl" "1.15.41"
"@swc/core-win32-arm64-msvc" "1.15.41"
"@swc/core-win32-ia32-msvc" "1.15.41"
"@swc/core-win32-x64-msvc" "1.15.41"
"@swc/core-darwin-arm64" "1.15.40"
"@swc/core-darwin-x64" "1.15.40"
"@swc/core-linux-arm-gnueabihf" "1.15.40"
"@swc/core-linux-arm64-gnu" "1.15.40"
"@swc/core-linux-arm64-musl" "1.15.40"
"@swc/core-linux-ppc64-gnu" "1.15.40"
"@swc/core-linux-s390x-gnu" "1.15.40"
"@swc/core-linux-x64-gnu" "1.15.40"
"@swc/core-linux-x64-musl" "1.15.40"
"@swc/core-win32-arm64-msvc" "1.15.40"
"@swc/core-win32-ia32-msvc" "1.15.40"
"@swc/core-win32-x64-msvc" "1.15.40"
"@swc/counter@^0.1.3":
version "0.1.3"
@@ -4922,110 +4812,110 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.61.0", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz#db20271974b94a3a54d3b9544e5f5b3481448400"
integrity sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==
"@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.61.0"
"@typescript-eslint/type-utils" "8.61.0"
"@typescript-eslint/utils" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.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.61.0", "@typescript-eslint/parser@^8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.0.tgz#1afe73c9ccce16b7a26d6b95f9400b0ccc34af87"
integrity sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==
"@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.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.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.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz#417a2feac32e8ebd336d63f068c3b42b736ea1ac"
integrity sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==
"@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.61.0"
"@typescript-eslint/types" "^8.61.0"
"@typescript-eslint/tsconfig-utils" "^8.60.1"
"@typescript-eslint/types" "^8.60.1"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz#93c2520d05653fe65eb9ee98efc74fd0134a7852"
integrity sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==
"@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.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
"@typescript-eslint/tsconfig-utils@8.61.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/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/tsconfig-utils@^8.61.0":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz#ca88080e0cf191d49516d7f300b67aa090d2254f"
integrity sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==
"@typescript-eslint/type-utils@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz#50219b57e6b89cecfb1a15f093b15ec9ee019974"
integrity sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==
"@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.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/utils" "8.61.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.61.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/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/types@^8.61.0":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.1.tgz#0c51f518e4e6848371a1c988e859d59eb7522d5a"
integrity sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==
"@typescript-eslint/typescript-estree@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz#98ca47260bbf627fc28f018b3a0abf00e3090690"
integrity sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==
"@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.61.0"
"@typescript-eslint/tsconfig-utils" "8.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.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.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f"
integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==
"@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.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.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.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz#39b4e1ab8936d23bea973d39fd092f9aa21f275e"
integrity sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==
"@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.61.0"
"@typescript-eslint/types" "8.60.1"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -5688,10 +5578,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.35, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.35"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz#f0f2232e0de2d2f82cc491bcf830b05ed05937c6"
integrity sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==
baseline-browser-mapping@^2.10.33, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.33"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz#27c299b096404978831958d429f48390424c4f9b"
integrity sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==
batch@0.6.1:
version "0.6.1"
@@ -5934,10 +5824,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001797:
version "1.0.30001797"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz#1332709e1439f01ff92085dd17001e0a45897ec0"
integrity sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001793:
version "1.0.30001793"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz#238887ddf5fcfc8c36d872394d0a78a517312a72"
integrity sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==
ccount@^2.0.0:
version "2.0.1"
@@ -9410,9 +9300,9 @@ jiti@^1.20.0:
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
joi@^17.9.2:
version "17.13.4"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.4.tgz#ad6153d97ce558eb3a3b593e0d43eab51df1c474"
integrity sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==
version "17.13.3"
resolved "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz"
integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==
dependencies:
"@hapi/hoek" "^9.3.0"
"@hapi/topo" "^5.1.0"
@@ -9642,12 +9532,12 @@ latest-version@^7.0.0:
package-json "^8.1.0"
launch-editor@^2.6.1:
version "2.14.1"
resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.14.1.tgz#f7e0da3f58aaea03fea01074d840b5f739ed7ddc"
integrity sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==
version "2.11.1"
resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz"
integrity sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==
dependencies:
picocolors "^1.1.1"
shell-quote "^1.8.4"
shell-quote "^1.8.3"
layout-base@^1.0.0:
version "1.0.2"
@@ -12270,10 +12160,10 @@ prettier-linter-helpers@^1.0.1:
dependencies:
fast-diff "^1.1.2"
prettier@^3.8.4:
version "3.8.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.4.tgz#f334f013ac04a96676f24dabc23c1c4ae1bae411"
integrity sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==
prettier@^3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.3.tgz#560f2de55bf01b4c0503bc629d5df99b9a1d09b0"
integrity sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==
pretty-error@^4.0.0:
version "4.0.0"
@@ -13599,7 +13489,7 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.8.4:
shell-quote@^1.8.3:
version "1.8.4"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190"
integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==
@@ -14499,15 +14389,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.61.0:
version "8.61.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz#6927fb94f5f29623e370d33fd9fa61f15d6d996b"
integrity sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==
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.61.0"
"@typescript-eslint/parser" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/utils" "8.61.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"
@@ -15006,9 +14896,9 @@ webpack-dev-middleware@^7.4.2:
schema-utils "^4.0.0"
webpack-dev-server@^5.2.2:
version "5.2.5"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.5.tgz#648fceaac6a5736b0935e5c1e55d6aa1d0626119"
integrity sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==
version "5.2.4"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz#6e6306ce59848ed322c235e48b326632b1eed6d6"
integrity sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==
dependencies:
"@types/bonjour" "^3.5.13"
"@types/connect-history-api-fallback" "^1.5.4"
+1 -1
View File
@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.16.3 # 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
+1 -1
View File
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.16.3](https://img.shields.io/badge/Version-0.16.3-informational?style=flat-square)
![Version: 0.16.0](https://img.shields.io/badge/Version-0.16.0-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
+1 -1
View File
@@ -126,7 +126,7 @@ spec:
{{- toYaml .Values.resources | nindent 12 }}
{{- end }}
{{- if .Values.supersetCeleryBeat.extraContainers }}
{{- tpl (toYaml .Values.supersetCeleryBeat.extraContainers) . | nindent 8 }}
{{- toYaml .Values.supersetCeleryBeat.extraContainers | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
@@ -121,7 +121,7 @@ spec:
{{- toYaml .Values.resources | nindent 12 }}
{{- end }}
{{- if .Values.supersetCeleryFlower.extraContainers }}
{{- tpl (toYaml .Values.supersetCeleryFlower.extraContainers) . | nindent 8 }}
{{- toYaml .Values.supersetCeleryFlower.extraContainers | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
@@ -141,7 +141,7 @@ spec:
{{- toYaml .Values.resources | nindent 12 }}
{{- end }}
{{- if .Values.supersetWorker.extraContainers }}
{{- tpl (toYaml .Values.supersetWorker.extraContainers) . | nindent 8 }}
{{- toYaml .Values.supersetWorker.extraContainers | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
+1 -1
View File
@@ -120,7 +120,7 @@ spec:
livenessProbe: {{- .Values.supersetWebsockets.livenessProbe | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.supersetWebsockets.extraContainers }}
{{- tpl (toYaml .Values.supersetWebsockets.extraContainers) . | nindent 8 }}
{{- toYaml .Values.supersetWebsockets.extraContainers | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
+1 -1
View File
@@ -151,7 +151,7 @@ spec:
{{- toYaml .Values.resources | nindent 12 }}
{{- end }}
{{- if .Values.supersetNode.extraContainers }}
{{- tpl (toYaml .Values.supersetNode.extraContainers) . | nindent 8 }}
{{- toYaml .Values.supersetNode.extraContainers | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
+1 -4
View File
@@ -62,9 +62,6 @@ spec:
{{- if .Values.init.initContainers }}
initContainers: {{- tpl (toYaml .Values.init.initContainers) . | nindent 6 }}
{{- end }}
{{- with .Values.hostAliases }}
hostAliases: {{- toYaml . | nindent 6 }}
{{- end }}
containers:
- name: {{ template "superset.name" . }}-init-db
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
@@ -104,7 +101,7 @@ spec:
command: {{ tpl (toJson .Values.init.command) . }}
resources: {{- toYaml .Values.init.resources | nindent 10 }}
{{- if .Values.init.extraContainers }}
{{- tpl (toYaml .Values.init.extraContainers) . | nindent 6 }}
{{- toYaml .Values.init.extraContainers | nindent 6 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
+21 -21
View File
@@ -43,7 +43,7 @@ dependencies = [
"click-option-group",
"colorama",
"flask-cors>=6.0.0, <7.0",
"croniter>=6.2.2",
"croniter>=0.3.28",
"cron-descriptor",
"cryptography>=42.0.4, <47.0.0",
"deprecation>=2.1.0, <2.2.0",
@@ -53,9 +53,9 @@ dependencies = [
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
"flask-login>=0.6.0, < 1.0",
"flask-migrate>=4.1.0, <5.0",
"flask-migrate>=3.1.0, <5.0",
"flask-session>=0.4.0, <1.0",
"flask-wtf>=1.3.0, <2.0",
"flask-wtf>=1.1.0, <2.0",
"geopy",
"greenlet>=3.0.3, <=3.5.0",
"gunicorn>=25.3.0, <26; sys_platform != 'win32'",
@@ -64,7 +64,7 @@ dependencies = [
"holidays>=0.45, <1",
"humanize",
"isodate",
"jsonpath-ng>=1.8.0, <2",
"jsonpath-ng>=1.6.1, <2",
"Mako>=1.2.2",
"markdown>=3.10.2",
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
@@ -80,7 +80,7 @@ dependencies = [
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
# --------------------------
"parsedatetime",
"paramiko>=3.4.0, <4.0", # 4.0 removed DSSKey, still referenced by sshtunnel
"paramiko>=3.4.0",
"pgsanity",
"Pillow>=11.0.0, <13",
"polyline>=2.0.0, <3.0",
@@ -94,10 +94,10 @@ dependencies = [
"PyJWT>=2.4.0, <3.0",
"redis>=5.0.0, <6.0",
"rison>=2.0.0, <3.0",
"selenium>=4.44.0, <5.0",
"selenium>=4.14.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=4.1.1",
"simplejson>=3.15.0",
"slack_sdk>=3.19.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
@@ -107,7 +107,7 @@ dependencies = [
"typing-extensions>=4, <5",
"waitress; sys_platform == 'win32'",
"watchdog>=6.0.0",
"wtforms>=3.2.2, <4",
"wtforms>=2.3.3, <4",
"wtforms-json",
"xlsxwriter>=3.2.9, <3.3",
]
@@ -118,10 +118,10 @@ athena = ["pyathena[pandas]>=2, <4"]
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.19.1",
"sqlalchemy-bigquery>=1.17.0",
"sqlalchemy-bigquery>=1.15.0",
"google-cloud-bigquery>=3.10.0",
]
clickhouse = ["clickhouse-connect>=1.1.1, <2.0"]
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
d1 = [
@@ -143,41 +143,41 @@ duckdb = ["duckdb>=1.5.2,<2", "duckdb-engine>=0.17.0"]
dynamodb = ["pydynamodb>=0.4.2"]
solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
exasol = ["sqlalchemy-exasol>=2.4.0, <8.0"]
excel = ["xlrd>=2.0.2, <2.1"]
exasol = ["sqlalchemy-exasol >= 2.4.0, < 8.0"]
excel = ["xlrd>=1.2.0, <1.3"]
fastmcp = [
"fastmcp>=3.2.4,<4.0",
# tiktoken backs the response-size-guard token estimator. Without
# it, the middleware falls back to a coarser character-based
# heuristic that under-counts JSON-heavy MCP responses.
"tiktoken>=0.13.0,<1.0",
"tiktoken>=0.7.0,<1.0",
]
firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=26.4.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
hana = ["hdbcli==2.28.21", "sqlalchemy_hana==0.4.0"]
hana = ["hdbcli==2.28.20", "sqlalchemy_hana==0.4.0"]
hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7.0",
"tableschema",
"thrift>=0.23.0, <1.0.0",
"thrift>=0.14.1, <1.0.0",
"thrift_sasl>=0.4.3, < 1.0.0",
]
impala = ["impyla>0.16.2, <0.23"]
kusto = ["sqlalchemy-kusto>=3.1.2, <4"]
kylin = ["kylinpy>=2.8.1, <2.9"]
mssql = ["pymssql>=2.3.13, <3"]
mssql = ["pymssql>=2.2.8, <3"]
# motherduck is an alias for duckdb - MotherDuck works via the duckdb driver
motherduck = ["apache-superset[duckdb]"]
mysql = ["mysqlclient>=2.1.0, <3"]
ocient = [
"sqlalchemy-ocient>=1.0.0",
"pyocient>=1.0.15, <4",
"pyocient>=1.0.15, <2",
"shapely",
"geojson",
]
oracle = ["oracledb>=2.0.0, <5"]
oracle = ["cx-Oracle>8.0.0, <8.4"]
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.60.0, <2"]
@@ -195,7 +195,7 @@ spark = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7",
"tableschema",
"thrift>=0.23.0, <1",
"thrift>=0.14.1, <1",
]
tdengine = [
"taospy>=2.7.21",
@@ -205,7 +205,7 @@ teradata = ["teradatasql>=16.20.0.23"]
thumbnails = [] # deprecated, will be removed in 7.0
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
netezza = ["nzalchemy>=11.0.2"]
starrocks = ["starrocks>=1.3.3, <2"]
starrocks = ["starrocks>=1.0.0"]
doris = ["pydoris>=1.0.0, <2.0.0"]
oceanbase = ["oceanbase_py>=0.0.1.2"]
ydb = ["ydb-sqlalchemy>=0.1.2", "ydb-sqlglot-plugin>=0.2.5"]
@@ -222,7 +222,7 @@ development = [
"pip",
"polib", # used by scripts/translations/ and their unit tests
"pre-commit",
"progress>=1.6.1,<2",
"progress>=1.5,<2",
"psutil",
"pyfakefs",
"pyinstrument>=5.1.2,<6",
+11 -9
View File
@@ -50,7 +50,7 @@ cattrs==25.1.1
# via requests-cache
celery==5.5.2
# via apache-superset (pyproject.toml)
certifi==2026.5.20
certifi==2025.6.15
# via
# requests
# selenium
@@ -84,7 +84,7 @@ colorama==0.4.6
# flask-appbuilder
cron-descriptor==1.4.5
# via apache-superset (pyproject.toml)
croniter==6.2.2
croniter==6.0.0
# via apache-superset (pyproject.toml)
cryptography==46.0.7
# via
@@ -141,7 +141,7 @@ flask-login==0.6.3
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
flask-migrate==4.1.0
flask-migrate==3.1.0
# via apache-superset (pyproject.toml)
flask-session==0.8.0
# via apache-superset (pyproject.toml)
@@ -151,7 +151,7 @@ flask-sqlalchemy==2.5.1
# flask-migrate
flask-talisman==1.1.0
# via apache-superset (pyproject.toml)
flask-wtf==1.3.0
flask-wtf==1.2.2
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
@@ -194,7 +194,7 @@ jinja2==3.1.6
# via
# flask
# flask-babel
jsonpath-ng==1.8.0
jsonpath-ng==1.7.0
# via apache-superset (pyproject.toml)
jsonschema==4.23.0
# via
@@ -286,6 +286,8 @@ pillow==12.2.0
# via apache-superset (pyproject.toml)
platformdirs==4.3.8
# via requests-cache
ply==3.11
# via jsonpath-ng
polyline==2.0.2
# via apache-superset (pyproject.toml)
prison==0.2.1
@@ -378,13 +380,13 @@ rpds-py==0.25.0
# referencing
rsa==4.9.1
# via google-auth
selenium==4.44.0
selenium==4.32.0
# via apache-superset (pyproject.toml)
setuptools==80.9.0
# via -r requirements/base.in
shillelagh==1.4.4
# via apache-superset (pyproject.toml)
simplejson==4.1.1
simplejson==3.20.1
# via apache-superset (pyproject.toml)
six==1.17.0
# via
@@ -421,7 +423,7 @@ sshtunnel==0.4.0
# via apache-superset (pyproject.toml)
tabulate==0.10.0
# via apache-superset (pyproject.toml)
trio==0.33.0
trio==0.30.0
# via
# selenium
# trio-websocket
@@ -478,7 +480,7 @@ wrapt==1.17.2
# via deprecated
wsproto==1.2.0
# via trio-websocket
wtforms==3.2.2
wtforms==3.2.1
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
+16 -12
View File
@@ -112,7 +112,7 @@ celery==5.5.2
# via
# -c requirements/base-constraint.txt
# apache-superset
certifi==2026.5.20
certifi==2025.6.15
# via
# -c requirements/base-constraint.txt
# httpcore
@@ -174,7 +174,7 @@ cron-descriptor==1.4.5
# via
# -c requirements/base-constraint.txt
# apache-superset
croniter==6.2.2
croniter==6.0.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -293,7 +293,7 @@ flask-login==0.6.3
# -c requirements/base-constraint.txt
# apache-superset
# flask-appbuilder
flask-migrate==4.1.0
flask-migrate==3.1.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -312,7 +312,7 @@ flask-talisman==1.1.0
# apache-superset
flask-testing==0.8.1
# via apache-superset
flask-wtf==1.3.0
flask-wtf==1.2.2
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -471,7 +471,7 @@ jmespath==1.1.0
# via
# boto3
# botocore
jsonpath-ng==1.8.0
jsonpath-ng==1.7.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -674,6 +674,10 @@ platformdirs==4.3.8
# virtualenv
pluggy==1.5.0
# via pytest
ply==3.11
# via
# -c requirements/base-constraint.txt
# jsonpath-ng
polib==1.2.0
# via apache-superset
polyline==2.0.2
@@ -686,7 +690,7 @@ prison==0.2.1
# via
# -c requirements/base-constraint.txt
# flask-appbuilder
progress==1.6.1
progress==1.6
# via apache-superset
prompt-toolkit==3.0.51
# via
@@ -921,7 +925,7 @@ s3transfer==0.16.0
# via boto3
secretstorage==3.5.0
# via keyring
selenium==4.44.0
selenium==4.32.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -939,7 +943,7 @@ shillelagh==1.4.4
# via
# -c requirements/base-constraint.txt
# apache-superset
simplejson==4.1.1
simplejson==3.20.1
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -976,7 +980,7 @@ sqlalchemy==1.4.54
# shillelagh
# sqlalchemy-bigquery
# sqlalchemy-utils
sqlalchemy-bigquery==1.17.0
sqlalchemy-bigquery==1.15.0
# via apache-superset
sqlalchemy-utils==0.42.0
# via
@@ -1007,7 +1011,7 @@ tabulate==0.10.0
# via
# -c requirements/base-constraint.txt
# apache-superset
tiktoken==0.13.0
tiktoken==0.12.0
# via apache-superset
tomli-w==1.2.0
# via apache-superset-extensions-cli
@@ -1019,7 +1023,7 @@ tqdm==4.67.1
# prophet
trino==0.330.0
# via apache-superset
trio==0.33.0
trio==0.30.0
# via
# -c requirements/base-constraint.txt
# selenium
@@ -1121,7 +1125,7 @@ wsproto==1.2.0
# via
# -c requirements/base-constraint.txt
# trio-websocket
wtforms==3.2.2
wtforms==3.2.1
# via
# -c requirements/base-constraint.txt
# apache-superset
+1 -1
View File
@@ -1 +1 @@
v24.16.0
v22.22.0
+148 -140
View File
@@ -27,6 +27,19 @@
"webpack-cli": "^5.1.4"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/cli": {
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.25.6.tgz",
@@ -58,12 +71,12 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.29.7",
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -72,30 +85,32 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
"integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.29.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.6.tgz",
"integrity": "sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA==",
"version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
"integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.6",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.29.2",
"@babel/parser": "^7.29.3",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/remapping": "^2.3.5",
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
"@babel/generator": "^7.25.0",
"@babel/helper-compilation-targets": "^7.25.2",
"@babel/helper-module-transforms": "^7.25.2",
"@babel/helpers": "^7.25.0",
"@babel/parser": "^7.25.0",
"@babel/template": "^7.25.0",
"@babel/traverse": "^7.25.2",
"@babel/types": "^7.25.2",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -111,13 +126,13 @@
}
},
"node_modules/@babel/generator": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7",
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -154,14 +169,15 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
"integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.29.7",
"@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
"@babel/compat-data": "^7.25.2",
"@babel/helper-validator-option": "^7.24.8",
"browserslist": "^4.23.1",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
@@ -366,28 +382,29 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
"integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -408,25 +425,26 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz",
"integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7"
"@babel/template": "^7.25.0",
"@babel/types": "^7.25.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
"dev": true,
"dependencies": {
"@babel/types": "^7.29.7"
"@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1825,14 +1843,14 @@
}
},
"node_modules/@babel/template": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7"
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -1857,13 +1875,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -2631,16 +2649,6 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
@@ -7975,6 +7983,16 @@
}
},
"dependencies": {
"@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"@babel/cli": {
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.25.6.tgz",
@@ -7993,38 +8011,38 @@
}
},
"@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.29.7",
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
}
},
"@babel/compat-data": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
"integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
"dev": true
},
"@babel/core": {
"version": "7.29.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.6.tgz",
"integrity": "sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA==",
"version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
"integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.6",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.29.2",
"@babel/parser": "^7.29.3",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/remapping": "^2.3.5",
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
"@babel/generator": "^7.25.0",
"@babel/helper-compilation-targets": "^7.25.2",
"@babel/helper-module-transforms": "^7.25.2",
"@babel/helpers": "^7.25.0",
"@babel/parser": "^7.25.0",
"@babel/template": "^7.25.0",
"@babel/traverse": "^7.25.2",
"@babel/types": "^7.25.2",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -8033,13 +8051,13 @@
}
},
"@babel/generator": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"dev": true,
"requires": {
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7",
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -8065,14 +8083,14 @@
}
},
"@babel/helper-compilation-targets": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
"integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
"dev": true,
"requires": {
"@babel/compat-data": "^7.29.7",
"@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
"@babel/compat-data": "^7.25.2",
"@babel/helper-validator-option": "^7.24.8",
"browserslist": "^4.23.1",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
}
@@ -8211,21 +8229,21 @@
}
},
"@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true
},
"@babel/helper-validator-option": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
"integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
"dev": true
},
"@babel/helper-wrap-function": {
@@ -8240,22 +8258,22 @@
}
},
"@babel/helpers": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz",
"integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
"dev": true,
"requires": {
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7"
"@babel/template": "^7.25.0",
"@babel/types": "^7.25.6"
}
},
"@babel/parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
"dev": true,
"requires": {
"@babel/types": "^7.29.7"
"@babel/types": "^7.29.0"
}
},
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
@@ -9139,14 +9157,14 @@
}
},
"@babel/template": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7"
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
}
},
"@babel/traverse": {
@@ -9165,13 +9183,13 @@
}
},
"@babel/types": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
}
},
"@bcoe/v8-coverage": {
@@ -9753,16 +9771,6 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@superset-ui/embedded-sdk",
"version": "0.4.0",
"version": "0.3.0",
"description": "SDK for embedding resources from Superset into your own application",
"access": "public",
"keywords": [
+34
View File
@@ -0,0 +1,34 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
**/*{.,-}min.js
**/*.sh
coverage/**
dist/*
src/assets/images/*
node_modules/*
node_modules*/*
vendor/*
docs/*
src/dashboard/deprecated/*
src/temp/*
**/node_modules
*.d.ts
coverage/
esm/
lib/
tmp/
storybook-static/
+523
View File
@@ -0,0 +1,523 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// Register TypeScript require hook so ESLint can load .ts plugin files
require('tsx/cjs');
const packageConfig = require('./package.json');
const importCoreModules = [];
Object.entries(packageConfig.dependencies).forEach(([pkg]) => {
if (/@superset-ui/.test(pkg)) {
importCoreModules.push(pkg);
}
});
// ignore files in production mode
let ignorePatterns = [];
if (process.env.NODE_ENV === 'production') {
ignorePatterns = [
'*.test.{js,ts,jsx,tsx}',
'plugins/**/test/**/*',
'packages/**/test/**/*',
'packages/generator-superset/**/*',
];
}
const restrictedImportsRules = {
'no-design-icons': {
name: '@ant-design/icons',
message:
'Avoid importing icons directly from @ant-design/icons. Use the src/components/Icons component instead.',
},
'no-moment': {
name: 'moment',
message:
'Please use the dayjs library instead of moment.js. See https://day.js.org',
},
'no-lodash-memoize': {
name: 'lodash/memoize',
message: 'Lodash Memoize is unsafe! Please use memoize-one instead',
},
'no-testing-library-react': {
name: '@superset-ui/core/spec',
message: 'Please use spec/helpers/testing-library instead',
},
'no-testing-library-react-dom-utils': {
name: '@testing-library/react-dom-utils',
message: 'Please use spec/helpers/testing-library instead',
},
'no-antd': {
name: 'antd',
message: 'Please import Ant components from the index of src/components',
},
'no-superset-theme': {
name: '@superset-ui/core',
importNames: ['supersetTheme'],
message:
'Please use the theme directly from the ThemeProvider rather than importing supersetTheme.',
},
'no-query-string': {
name: 'query-string',
message: 'Please use the URLSearchParams API instead of query-string.',
},
'no-jest-mock-console': {
name: 'jest-mock-console',
message: 'Please use native Jest spies, i.e. jest.spyOn(console, "warn")',
},
};
module.exports = {
extends: [
'eslint:recommended',
'plugin:import/recommended',
'plugin:react-prefer-function-component/recommended',
'plugin:storybook/recommended',
'prettier',
],
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
requireConfigFile: false,
babelOptions: {
presets: ['@babel/preset-react', '@babel/preset-env'],
},
},
env: {
browser: true,
node: true,
es2020: true,
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
moduleDirectory: ['node_modules', '.'],
},
typescript: {
alwaysTryTypes: true,
project: [
'./tsconfig.json',
'./packages/superset-ui-core/tsconfig.json',
'./packages/superset-ui-chart-controls/',
'./plugins/*/tsconfig.json',
],
},
},
'import/core-modules': importCoreModules,
react: {
version: 'detect',
},
},
plugins: [
'import',
'lodash',
'theme-colors',
'icons',
'i18n-strings',
'react-prefer-function-component',
'react-you-might-not-need-an-effect',
'prettier',
],
rules: {
// === Essential Superset customizations ===
// Prettier integration
'prettier/prettier': 'error',
// Custom Superset rules
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': 'error',
// Core ESLint overrides for Superset
'no-console': 'warn',
'no-unused-vars': 'off', // TypeScript handles this
camelcase: [
'error',
{
allow: ['^UNSAFE_', '__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'],
properties: 'never',
},
],
'prefer-destructuring': ['error', { object: true, array: false }],
'no-prototype-builtins': 0,
curly: 'off',
// Import plugin overrides
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-cycle': 0,
'import/prefer-default-export': 0,
'import/no-named-as-default-member': 0,
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'test/**',
'tests/**',
'spec/**',
'**/__tests__/**',
'**/__mocks__/**',
'*.test.{js,jsx,ts,tsx}',
'*.spec.{js,jsx,ts,tsx}',
'**/*.test.{js,jsx,ts,tsx}',
'**/*.spec.{js,jsx,ts,tsx}',
'**/jest.config.js',
'**/jest.setup.js',
'**/webpack.config.js',
'**/webpack.config.*.js',
'**/.eslintrc*.js',
],
optionalDependencies: false,
},
],
// React plugin overrides
'react-prefer-function-component/react-prefer-function-component': 1,
// React effect best practices
'react-you-might-not-need-an-effect/no-empty-effect': 'error',
'react-you-might-not-need-an-effect/no-pass-live-state-to-parent': 'error',
'react-you-might-not-need-an-effect/no-initialize-state': 'error',
// Lodash
'lodash/import-scope': [2, 'member'],
// React effect best practices
'react-you-might-not-need-an-effect/no-reset-all-state-on-prop-change':
'error',
'react-you-might-not-need-an-effect/no-chain-state-updates': 'error',
'react-you-might-not-need-an-effect/no-event-handler': 'error',
'react-you-might-not-need-an-effect/no-derived-state': 'error',
// Storybook
'storybook/prefer-pascal-case': 'error',
// File progress
'file-progress/activate': 1,
// React effect rules
'react-you-might-not-need-an-effect/no-adjust-state-on-prop-change':
'error',
'react-you-might-not-need-an-effect/no-pass-data-to-parent': 'error',
// Restricted imports
'no-restricted-imports': [
'error',
{
paths: Object.values(restrictedImportsRules).filter(Boolean),
patterns: ['antd/*'],
},
],
// Temporarily disabled for migration
'no-unsafe-optional-chaining': 0,
'no-import-assign': 0,
'import/no-relative-packages': 0,
'no-promise-executor-return': 0,
'import/no-import-module-exports': 0,
// Restrict certain syntax patterns
'no-restricted-syntax': [
'error',
{
selector:
"ImportDeclaration[source.value='react'] :matches(ImportDefaultSpecifier, ImportNamespaceSpecifier)",
message:
'Default React import is not required due to automatic JSX runtime in React 16.4',
},
{
selector: 'ImportNamespaceSpecifier[parent.source.value!=/^(\\.|src)/]',
message: 'Wildcard imports are not allowed',
},
],
},
overrides: [
// Ban JavaScript files in src/ - all new code must be TypeScript
{
files: ['src/**/*.js', 'src/**/*.jsx'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message:
'JavaScript files are not allowed in src/. Please use TypeScript (.ts/.tsx) instead.',
},
],
},
},
// Ban JavaScript files in plugins/ - all plugin source code must be TypeScript
{
files: ['plugins/**/src/**/*.js', 'plugins/**/src/**/*.jsx'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message:
'JavaScript files are not allowed in plugins/. Please use TypeScript (.ts/.tsx) instead.',
},
],
},
},
// Ban JavaScript files in packages/ - with exceptions for config files and generators
{
files: ['packages/**/src/**/*.js', 'packages/**/src/**/*.jsx'],
excludedFiles: [
'packages/generator-superset/**/*', // Yeoman generator templates run via Node
'packages/**/__mocks__/**/*', // Test mocks
],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message:
'JavaScript files are not allowed in packages/. Please use TypeScript (.ts/.tsx) instead.',
},
],
},
},
{
files: ['*.ts', '*.tsx'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: ['plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['@typescript-eslint/eslint-plugin'],
rules: {
// TypeScript-specific rule overrides
'@typescript-eslint/ban-ts-ignore': 0,
'@typescript-eslint/ban-ts-comment': 0,
'@typescript-eslint/ban-types': 0,
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'enum',
format: ['PascalCase'],
},
{
selector: 'enumMember',
format: ['PascalCase'],
},
],
'@typescript-eslint/no-empty-function': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-use-before-define': 'error',
'@typescript-eslint/no-non-null-assertion': 0,
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/prefer-optional-chain': 'error',
// Disable base rules that conflict with TS versions
'no-unused-vars': 'off',
'no-use-before-define': 'off',
'no-shadow': 'off',
// Import overrides for TypeScript
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
},
settings: {
'import/resolver': {
typescript: {},
},
},
},
{
files: ['packages/**'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
'no-restricted-imports': [
'error',
{
paths: [
restrictedImportsRules['no-moment'],
restrictedImportsRules['no-lodash-memoize'],
restrictedImportsRules['no-superset-theme'],
],
patterns: [],
},
],
},
},
{
files: ['plugins/**'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
restrictedImportsRules['no-moment'],
restrictedImportsRules['no-lodash-memoize'],
],
patterns: [],
},
],
},
},
{
files: ['src/components/**', 'src/theme/**'],
rules: {
'no-restricted-imports': [
'error',
{
paths: Object.values(restrictedImportsRules).filter(
r => r.name !== 'antd',
),
patterns: [],
},
],
},
},
{
files: [
'*.test.ts',
'*.test.tsx',
'*.test.js',
'*.test.jsx',
'*.stories.tsx',
'*.stories.jsx',
'fixtures.*',
'**/test/**/*',
'**/tests/**/*',
'spec/**/*',
'**/fixtures/**/*',
'**/__mocks__/**/*',
'**/spec/**/*',
],
excludedFiles: 'cypress-base/cypress/**/*',
plugins: ['jest-dom', 'no-only-tests', 'testing-library'],
extends: ['plugin:jest-dom/recommended', 'plugin:testing-library/react'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
'prefer-promise-reject-errors': 0,
'max-classes-per-file': 0,
// Temporary for migration
'testing-library/await-async-queries': 0,
'testing-library/await-async-utils': 0,
'testing-library/no-await-sync-events': 0,
'testing-library/no-render-in-lifecycle': 0,
'testing-library/no-unnecessary-act': 0,
'testing-library/no-wait-for-multiple-assertions': 0,
'testing-library/prefer-screen-queries': 0,
'testing-library/await-async-events': 0,
'testing-library/no-node-access': 0,
'testing-library/no-wait-for-side-effects': 0,
'testing-library/prefer-presence-queries': 0,
'testing-library/render-result-naming-convention': 0,
'testing-library/no-container': 0,
'testing-library/prefer-find-by': 0,
'testing-library/no-manual-cleanup': 0,
'no-restricted-syntax': [
'error',
{
selector:
"ImportDeclaration[source.value='react'] :matches(ImportDefaultSpecifier, ImportNamespaceSpecifier)",
message:
'Default React import is not required due to automatic JSX runtime in React 16.4',
},
],
'no-restricted-imports': 0,
},
},
{
files: [
'*.test.ts',
'*.test.tsx',
'*.test.js',
'*.test.jsx',
'*.stories.tsx',
'*.stories.jsx',
'fixtures.*',
'**/test/**/*',
'**/tests/**/*',
'spec/**/*',
'**/fixtures/**/*',
'**/__mocks__/**/*',
'**/spec/**/*',
'cypress-base/cypress/**/*',
'Stories.tsx',
'packages/superset-ui-core/src/theme/index.tsx',
],
rules: {
'theme-colors/no-literal-colors': 0,
'icons/no-fa-icons-usage': 0,
'i18n-strings/no-template-vars': 0,
'no-restricted-imports': 0,
},
},
{
files: [
'packages/**/*.stories.*',
'packages/**/*.overview.*',
'packages/**/fixtures.*',
],
rules: {
'import/no-extraneous-dependencies': 'off',
},
},
{
files: ['playwright/**/*.ts', 'playwright/**/*.js'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
},
},
],
ignorePatterns,
};
+124
View File
@@ -0,0 +1,124 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// Register TypeScript require hook so ESLint can load .ts plugin files
require('tsx/cjs');
/**
* MINIMAL ESLint config - ONLY for rules OXC doesn't support
* This config is designed to be run alongside OXC linter
*
* Only covers:
* - Custom Superset plugins (theme-colors, icons, i18n)
* - Prettier formatting
* - File progress indicator
*/
module.exports = {
root: true,
// Don't report on eslint-disable comments for rules we don't have
reportUnusedDisableDirectives: false,
// Simple parser - no TypeScript needed since OXC handles that
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
requireConfigFile: false,
babelOptions: {
presets: ['@babel/preset-react', '@babel/preset-env'],
},
},
env: {
browser: true,
node: true,
es2020: true,
},
plugins: [
// ONLY custom Superset plugins that OXC doesn't support
'theme-colors',
'icons',
'i18n-strings',
'file-progress',
'prettier',
],
rules: {
// === ONLY rules that OXC cannot handle ===
// Prettier integration (formatting)
'prettier/prettier': 'error',
// Custom Superset plugins
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': 'error',
'file-progress/activate': 1,
// Explicitly turn off all other rules to avoid conflicts
// when the config gets merged with other configs
'import/no-unresolved': 'off',
'import/extensions': 'off',
'@typescript-eslint/naming-convention': 'off',
},
overrides: [
{
// Disable custom rules in test/story files
files: [
'**/*.test.*',
'**/*.spec.*',
'**/*.stories.*',
'**/test/**',
'**/tests/**',
'**/spec/**',
'**/__tests__/**',
'**/__mocks__/**',
'cypress-base/**',
'packages/superset-ui-core/src/theme/index.tsx',
],
rules: {
'theme-colors/no-literal-colors': 0,
'icons/no-fa-icons-usage': 0,
'i18n-strings/no-template-vars': 0,
'file-progress/activate': 0,
},
},
],
// Only check src/ files where theme/icon rules matter
ignorePatterns: [
'node_modules',
'dist',
'build',
'.next',
'coverage',
'*.min.js',
'vendor',
// Skip packages/plugins since they have different theming rules
'packages/**',
'plugins/**',
// Skip generated/external files
'*.generated.*',
'*.config.js',
'webpack.*',
// Temporary analysis files
'*.js', // Skip all standalone JS files in root
'*.json',
],
};
+1 -1
View File
@@ -1 +1 @@
v24.16.0
v22.22.0
+1 -8
View File
@@ -48,7 +48,6 @@ module.exports = {
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-export-namespace-from',
['@babel/plugin-transform-class-properties', { loose: true }],
'@babel/plugin-transform-class-static-block',
['@babel/plugin-transform-optional-chaining', { loose: true }],
['@babel/plugin-transform-private-methods', { loose: true }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],
@@ -107,13 +106,7 @@ module.exports = {
[
'babel-plugin-jsx-remove-data-test-id',
{
// The plugin matches attribute names exactly (no prefix match),
// so each data-test* attribute must be listed explicitly.
attributes: [
'data-test',
'data-test-drag-source-id',
'data-test-drop-target-id',
],
attributes: 'data-test',
},
],
],
+21 -67
View File
@@ -2058,24 +2058,6 @@
"node": ">=8"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz",
@@ -2952,8 +2934,6 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"peer": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
@@ -4373,8 +4353,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"peer": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
@@ -4708,15 +4686,16 @@
}
},
"node_modules/form-data": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.4",
"mime-types": "^2.1.35"
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
@@ -5028,9 +5007,10 @@
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -5614,9 +5594,7 @@
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@@ -7778,9 +7756,7 @@
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true,
"peer": true
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"node_modules/sshpk": {
"version": "1.18.0",
@@ -10228,20 +10204,6 @@
"get-package-type": "^0.1.0",
"js-yaml": "^3.13.1",
"resolve-from": "^5.0.0"
},
"dependencies": {
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"js-yaml": {
"version": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"requires": {
"argparse": "^2.0.1"
}
}
}
},
"@istanbuljs/schema": {
@@ -11044,8 +11006,6 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"peer": true,
"requires": {
"sprintf-js": "~1.0.2"
}
@@ -12091,9 +12051,7 @@
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"peer": true
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"esquery": {
"version": "1.4.0",
@@ -12342,15 +12300,15 @@
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="
},
"form-data": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.4",
"mime-types": "^2.1.35"
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
}
},
"fromentries": {
@@ -12571,9 +12529,9 @@
}
},
"hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"requires": {
"function-bind": "^1.1.2"
}
@@ -12995,8 +12953,6 @@
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"peer": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@@ -14507,9 +14463,7 @@
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true,
"peer": true
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sshpk": {
"version": "1.18.0",
@@ -65,86 +65,6 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
};
},
},
'no-eager-t-in-config': {
meta: {
type: 'problem',
fixable: 'code',
docs: {
description:
'Disallow eager t()/tn() calls for `label` and `description` in config objects evaluated at module load (e.g., controlPanel files). The translation is captured at module-evaluation time, before i18n has loaded, and never updates when the user switches language. Wrap the call in an arrow function so it is evaluated at render time.',
},
schema: [
{
type: 'object',
properties: {
properties: {
type: 'array',
items: { type: 'string' },
},
},
additionalProperties: false,
},
],
messages: {
eager:
'Eager `{{property}}: {{fn}}(...)` is evaluated at module load, before i18n is initialized. Wrap in an arrow function: `{{property}}: () => {{fn}}(...)`.',
},
},
create(context: Rule.RuleContext): Rule.RuleListener {
const watchedProps: string[] = context.options[0]?.properties ?? [
'label',
'description',
];
const TRANSLATE_FNS = new Set(['t', 'tn']);
function handler(node: Node): void {
const prop = node as Node & {
key: { type: string; name?: string; value?: string };
value: Node & {
type: string;
callee?: { type: string; name?: string };
};
shorthand?: boolean;
computed?: boolean;
};
if (prop.shorthand || prop.computed) return;
const keyName =
prop.key.type === 'Identifier'
? prop.key.name
: prop.key.type === 'Literal'
? prop.key.value
: undefined;
if (typeof keyName !== 'string' || !watchedProps.includes(keyName)) {
return;
}
const callee = prop.value;
if (
callee.type !== 'CallExpression' ||
callee.callee?.type !== 'Identifier' ||
!callee.callee.name ||
!TRANSLATE_FNS.has(callee.callee.name)
) {
return;
}
context.report({
node: prop.value,
messageId: 'eager',
data: { property: keyName, fn: callee.callee.name },
fix(fixer) {
const source = context.getSourceCode().getText(prop.value);
return fixer.replaceText(prop.value, `() => ${source}`);
},
});
}
return {
Property: handler,
};
},
},
'sentence-case-buttons': {
meta: {
type: 'suggestion',
@@ -1,86 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { Rule } from 'eslint';
const { RuleTester } = require('eslint');
const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
const ruleTester = new RuleTester({
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
});
const rule: Rule.RuleModule = plugin.rules['no-eager-t-in-config'];
ruleTester.run('no-eager-t-in-config', rule, {
valid: [
// Lazy form — the recommended pattern
"const c = { label: () => t('Foo') };",
"const c = { description: () => t('Foo') };",
"const c = { label: () => tn('one', 'many', n) };",
// Static strings — no translation, no issue
"const c = { label: 'Foo' };",
// Other property names — unaffected
"const c = { name: t('Foo') };",
"const c = { title: t('Foo') };",
// Computed keys are too dynamic to lint usefully
"const c = { [labelKey]: t('Foo') };",
// Shorthand: `{ label }` — no value to inspect
'const label = t("Foo"); const c = { label };',
// t() called inside a function body — already lazy
"const c = { label: state => t('Foo') };",
// Non-t() call expressions are fine
"const c = { label: someOtherFn('Foo') };",
],
invalid: [
{
code: "const c = { label: t('Foo') };",
output: "const c = { label: () => t('Foo') };",
errors: [{ messageId: 'eager' }],
},
{
code: "const c = { description: t('Foo bar') };",
output: "const c = { description: () => t('Foo bar') };",
errors: [{ messageId: 'eager' }],
},
{
code: "const c = { label: tn('one', 'many', 2) };",
output: "const c = { label: () => tn('one', 'many', 2) };",
errors: [{ messageId: 'eager' }],
},
// String-literal keys are equivalent to identifier keys
{
code: "const c = { 'label': t('Foo') };",
output: "const c = { 'label': () => t('Foo') };",
errors: [{ messageId: 'eager' }],
},
// Custom watched-property list via rule option
{
code: "const c = { headerTitle: t('Foo') };",
output: "const c = { headerTitle: () => t('Foo') };",
options: [{ properties: ['headerTitle'] }],
errors: [{ messageId: 'eager' }],
},
// Nested config — fires per occurrence
{
code: "const c = { foo: { label: t('A'), description: t('B') } };",
output:
"const c = { foo: { label: () => t('A'), description: () => t('B') } };",
errors: [{ messageId: 'eager' }, { messageId: 'eager' }],
},
],
});
@@ -31,13 +31,12 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
// Tests
//------------------------------------------------------------------------------
const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 6 } });
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
const rule: Rule.RuleModule = plugin.rules['no-template-vars'];
const errors: Array<{ message: string }> = [
const errors: Array<{ type: string }> = [
{
message:
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it can't handle strings that include variables",
type: 'CallExpression',
},
];
@@ -31,10 +31,7 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
// Tests
//------------------------------------------------------------------------------
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 6,
parserOptions: { ecmaFeatures: { jsx: true } },
},
parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } },
});
const rule: Rule.RuleModule = plugin.rules['no-fa-icons-usage'];
-137
View File
@@ -1,137 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* MINIMAL ESLint flat config - ONLY for rules OXC doesn't support.
*
* This config is run alongside the OXC (oxlint) linter, which handles the
* bulk of linting. ESLint here only covers the custom Superset plugins and
* Prettier formatting that oxlint cannot express. It is consumed by
* `scripts/oxlint-metrics-uploader.js` (`npm run lint-stats`).
*
* Migrated from the legacy `.eslintrc.minimal.js` (eslintrc) format to flat
* config for ESLint v9+/v10, where eslintrc is no longer supported.
*
* Only covers:
* - Custom Superset plugins (theme-colors, icons, i18n-strings)
* - Prettier formatting
*/
// Register the TypeScript require hook so ESLint can load the .ts plugin files
// from eslint-rules/*.
require('tsx/cjs');
const tsParser = require('@typescript-eslint/parser');
const prettierPlugin = require('eslint-plugin-prettier');
const themeColorsPlugin = require('eslint-plugin-theme-colors');
const iconsPlugin = require('eslint-plugin-icons');
const i18nStringsPlugin = require('eslint-plugin-i18n-strings');
module.exports = [
// Files this config applies to. Flat config has no `--ext`; globs live here.
// Only check src/ files where the theme/icon/i18n rules matter.
{
ignores: [
'node_modules/**',
'dist/**',
'build/**',
'.next/**',
'coverage/**',
'**/*.min.js',
'vendor/**',
// Skip packages/plugins since they have different theming rules
'packages/**',
'plugins/**',
// Skip generated/external/config files
'**/*.generated.*',
'**/*.config.js',
'**/webpack.*',
'*.json',
],
},
{
files: ['**/*.{js,jsx,ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module',
// The @typescript-eslint parser handles both TS/TSX and plain JS/JSX and
// is compatible with ESLint v10's scope manager. (The legacy
// @babel/eslint-parser does not support ESLint v10.) The custom rules
// here are pure AST visitors and do not require type information, so no
// `project` is configured — this keeps parsing fast.
parser: tsParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
// Don't report on eslint-disable comments for rules we don't have.
linterOptions: {
reportUnusedDisableDirectives: false,
},
plugins: {
prettier: prettierPlugin,
'theme-colors': themeColorsPlugin,
icons: iconsPlugin,
'i18n-strings': i18nStringsPlugin,
},
rules: {
// Prettier integration (formatting)
'prettier/prettier': 'error',
// Custom Superset plugins
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': 'error',
// Enabled only for controlPanel files via the override below.
'i18n-strings/no-eager-t-in-config': 'off',
},
},
{
// Eager t()/tn() in `label`/`description` config props is captured at
// module-load time, before i18n initializes — labels stay in the fallback
// language even after the user switches. Surfaced as a warning (with
// autofix to `() => t(...)`) wherever this is a real foot-gun:
// controlPanel files. Promote to `'error'` once the codebase is clean.
files: ['**/controlPanel.{ts,tsx,js,jsx}'],
rules: {
'i18n-strings/no-eager-t-in-config': 'warn',
},
},
{
// Disable custom rules in test/story files
files: [
'**/*.test.*',
'**/*.spec.*',
'**/*.stories.*',
'**/test/**',
'**/tests/**',
'**/spec/**',
'**/__tests__/**',
'**/__mocks__/**',
'cypress-base/**',
],
rules: {
'theme-colors/no-literal-colors': 'off',
'icons/no-fa-icons-usage': 'off',
'i18n-strings/no-template-vars': 'off',
},
},
];
+5 -7
View File
@@ -287,15 +287,13 @@
"ignorePatterns": [
"packages/generator-superset/**/*",
"cypress-base/**",
"**/node_modules/**",
"node_modules/**",
"build/**",
"**/dist/**",
"**/lib/**",
"**/esm/**",
"**/*.min.js",
"**/*.d.ts",
"dist/**",
"lib/**",
"esm/**",
"*.min.js",
"coverage/**",
"storybook-static/**",
".git/**",
"**/*.config.js",
"**/*.config.ts"
+1513 -1314
View File
File diff suppressed because it is too large Load Diff
+32 -43
View File
@@ -154,6 +154,7 @@
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
"@tanstack/react-router": "^1.170.15",
"@types/d3-format": "^3.0.1",
"@types/d3-selection": "^3.0.11",
"@types/d3-time-format": "^4.0.3",
@@ -178,14 +179,14 @@
"echarts": "^5.6.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.3.5",
"fuse.js": "^7.4.2",
"fuse.js": "^7.3.0",
"geolib": "^3.3.14",
"geostyler": "^18.6.0",
"geostyler-data": "^1.1.0",
"geostyler-openlayers-parser": "^5.7.0",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.7.0",
"google-auth-library": "^10.6.2",
"immer": "^11.1.8",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -203,13 +204,13 @@
"ol": "^10.9.0",
"query-string": "9.4.0",
"re-resizable": "^6.11.2",
"react": "^18.3.0",
"react-arborist": "^3.10.1",
"react": "^18.2.0",
"react-arborist": "^3.8.0",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^18.3.0",
"react-dom": "^18.2.0",
"react-google-recaptcha": "^3.1.0",
"react-intersection-observer": "^10.0.3",
"react-json-tree": "^0.20.0",
@@ -218,7 +219,6 @@
"react-redux": "^7.2.9",
"react-resize-detector": "^9.1.1",
"react-reverse-portal": "^2.3.0",
"react-router-dom": "^5.3.4",
"react-search-input": "^0.11.3",
"react-split": "^2.0.9",
"react-table": "^7.8.0",
@@ -261,16 +261,16 @@
"@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.14",
"@formatjs/intl-durationformat": "^0.10.3",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.60.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.3",
"@storybook/addon-links": "10.4.3",
"@storybook/react-webpack5": "10.4.3",
"@storybook/addon-docs": "10.4.1",
"@storybook/addon-links": "10.4.1",
"@storybook/react-webpack5": "10.4.1",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.41",
"@swc/core": "^1.15.40",
"@swc/plugin-emotion": "^14.12.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4",
@@ -280,16 +280,15 @@
"@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7",
"@types/jest": "^30.0.0",
"@types/jquery": "^4.0.1",
"@types/jquery": "^4.0.0",
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.9.2",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/node": "^25.9.1",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.12",
"@types/react-window": "^1.8.8",
"@types/redux-localstorage": "^1.0.8",
@@ -297,22 +296,22 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "^8.61.0",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.59.4",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"baseline-browser-mapping": "^2.10.34",
"baseline-browser-mapping": "^2.10.33",
"cheerio": "1.2.0",
"concurrently": "^10.0.3",
"copy-webpack-plugin": "^14.0.0",
"cross-env": "^10.1.0",
"css-loader": "^7.1.4",
"css-minimizer-webpack-plugin": "^8.0.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint": "^8.56.0",
"eslint-config-prettier": "^7.2.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-cypress": "^3.6.0",
@@ -320,12 +319,12 @@
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-lodash": "^8.0.0",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.0",
"eslint-plugin-storybook": "10.4.2",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.4",
"eslint-plugin-storybook": "10.4.1",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -344,19 +343,18 @@
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
"open-cli": "^9.0.0",
"oxlint": "^1.69.0",
"oxlint": "^1.68.0",
"po2json": "^0.4.5",
"prettier": "3.8.4",
"prettier": "3.8.3",
"prettier-plugin-packagejson": "^3.0.2",
"process": "^0.11.10",
"react-dnd-test-backend": "^11.1.3",
"react-refresh": "^0.18.0",
"react-resizable": "^4.0.1",
"redux-mock-store": "^1.5.4",
"source-map": "^0.7.6",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.4.3",
"storybook": "10.4.1",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.1",
@@ -369,9 +367,9 @@
"wait-on": "^9.0.10",
"webpack": "^5.107.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^7.0.3",
"webpack-dev-server": "^5.2.5",
"webpack-manifest-plugin": "^6.0.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.4",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.5.0",
"webpack-visualizer-plugin2": "^2.0.0"
},
@@ -383,8 +381,8 @@
"regenerator-runtime": "^0.14.1"
},
"engines": {
"node": "^24.16.0",
"npm": "^11.13.0"
"node": "^22.22.0",
"npm": "^10.8.1"
},
"overrides": {
"uuid": "$uuid",
@@ -415,16 +413,7 @@
"@jest/types": "^30.4.0",
"jest-util": "^30.4.0",
"jest-circus": "^30.4.0",
"jest-environment-node": "^30.4.0",
"@babel/eslint-parser": {
"eslint": "$eslint"
},
"eslint-plugin-import": {
"eslint": "$eslint"
},
"eslint-plugin-jest-dom": {
"eslint": "$eslint"
}
"jest-environment-node": "^30.4.0"
},
"readme": "ERROR: No README data found!",
"scarfSettings": {
@@ -37,7 +37,7 @@
"cross-env": "^10.1.0",
"fs-extra": "^11.3.5",
"jest": "^30.4.2",
"yeoman-test": "^11.5.3"
"yeoman-test": "^11.5.2"
},
"engines": {
"npm": ">= 4.0.0",
@@ -97,8 +97,8 @@
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.6",
"nanoid": "^5.0.9",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*",
"lodash": "^4.18.1",
@@ -37,7 +37,7 @@
* ```
*/
import { Disposable, Event } from '../common';
import { Disposable } from '../common';
/**
* Represents a menu item that links a view to a command.
@@ -102,37 +102,3 @@ export declare function registerMenuItem(
* ```
*/
export declare function getMenu(location: string): Menu | undefined;
/**
* Event fired when a menu item is registered.
*/
export interface MenuItemRegisteredEvent {
/** The menu item that was registered. */
item: MenuItem;
/** The location where the item was registered. */
location: string;
/** The group the item was placed in. */
group: 'primary' | 'secondary' | 'context';
}
/**
* Event fired when a menu item is unregistered.
*/
export interface MenuItemUnregisteredEvent {
/** The menu item that was unregistered. */
item: MenuItem;
/** The location where the item was registered. */
location: string;
/** The group the item was placed in. */
group: 'primary' | 'secondary' | 'context';
}
/**
* Event fired when a menu item is registered.
*/
export declare const onDidRegisterMenuItem: Event<MenuItemRegisteredEvent>;
/**
* Event fired when a menu item is unregistered.
*/
export declare const onDidUnregisterMenuItem: Event<MenuItemUnregisteredEvent>;
@@ -26,7 +26,7 @@ test('t() warns and creates a default translator when called before configure',
const { t } = require('./TranslatorSingleton');
const result = t('hello');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringMatching(/was called before configure\(\)/),
'You should call configure(...) before calling other methods',
);
expect(result).toBe('hello');
consoleSpy.mockRestore();
@@ -54,7 +54,7 @@ test('resetTranslation resets the configured singleton', () => {
// After reset, calling t() should warn again
t('hello');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringMatching(/was called before configure\(\)/),
'You should call configure(...) before calling other methods',
);
consoleSpy.mockRestore();
});
@@ -96,69 +96,6 @@ test('tn() calls translateWithNumber on the singleton', () => {
});
});
test('pre-configure warning fires once per unique key', () => {
jest.isolateModules(() => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { t } = require('./TranslatorSingleton');
t('apple');
t('apple');
t('apple');
t('banana');
expect(consoleSpy).toHaveBeenCalledTimes(2);
expect(consoleSpy).toHaveBeenNthCalledWith(
1,
expect.stringContaining('"apple"'),
);
expect(consoleSpy).toHaveBeenNthCalledWith(
2,
expect.stringContaining('"banana"'),
);
consoleSpy.mockRestore();
});
});
test('pre-configure warning suggests the lazy-function fix', () => {
jest.isolateModules(() => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { t } = require('./TranslatorSingleton');
t('Sort ascending');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('() => t("Sort ascending")'),
);
consoleSpy.mockRestore();
});
});
test('pre-configure warning is suppressed in production', () => {
jest.isolateModules(() => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { t } = require('./TranslatorSingleton');
t('hello');
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
if (originalEnv !== undefined) {
process.env.NODE_ENV = originalEnv;
} else {
delete process.env.NODE_ENV;
}
});
});
test('resetTranslation clears the warned-keys dedupe set', () => {
jest.isolateModules(() => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { t, resetTranslation } = require('./TranslatorSingleton');
t('hello');
expect(consoleSpy).toHaveBeenCalledTimes(1);
resetTranslation();
t('hello');
expect(consoleSpy).toHaveBeenCalledTimes(2);
consoleSpy.mockRestore();
});
});
test('resetTranslation does nothing when not yet configured', () => {
jest.isolateModules(() => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
@@ -168,7 +105,7 @@ test('resetTranslation does nothing when not yet configured', () => {
// The singleton is still unconfigured, so t() warns
t('hello');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringMatching(/was called before configure\(\)/),
'You should call configure(...) before calling other methods',
);
consoleSpy.mockRestore();
});
@@ -25,10 +25,6 @@ import { TranslatorConfig, Translations, LocaleData } from './types';
let singleton: Translator | undefined;
let isConfigured = false;
// Tracks which keys have already triggered a pre-configure warning so the
// logs don't drown in repeated calls from large module-load fan-outs.
const warnedPreConfigureKeys = new Set<string>();
function configure(config?: TranslatorConfig) {
singleton = new Translator(config);
isConfigured = true;
@@ -37,6 +33,10 @@ function configure(config?: TranslatorConfig) {
}
function getInstance() {
if (!isConfigured) {
console.warn('You should call configure(...) before calling other methods');
}
if (typeof singleton === 'undefined') {
singleton = new Translator();
}
@@ -44,32 +44,11 @@ function getInstance() {
return singleton;
}
function warnPreConfigure(fn: 't' | 'tn', key: string) {
// Only warn in non-production builds — production callers may legitimately
// tolerate the fallback, and the noise isn't useful at runtime.
if (
typeof process !== 'undefined' &&
process.env?.NODE_ENV === 'production'
) {
return;
}
if (warnedPreConfigureKeys.has(key)) return;
warnedPreConfigureKeys.add(key);
console.warn(
`[i18n] ${fn}(${JSON.stringify(key)}) was called before configure() — ` +
`the result is the fallback language and will not update when the ` +
`user switches language. If this call is at module load (e.g., a ` +
`controlPanel \`label\`/\`description\`), wrap it in an arrow ` +
`function: \`() => ${fn}(${JSON.stringify(key)})\`.`,
);
}
function resetTranslation() {
if (isConfigured) {
isConfigured = false;
singleton = undefined;
}
warnedPreConfigureKeys.clear();
}
function addTranslation(key: string, translations: string[]) {
@@ -85,12 +64,10 @@ function addLocaleData(data: LocaleData) {
}
function t(input: string, ...args: unknown[]) {
if (!isConfigured) warnPreConfigure('t', input);
return getInstance().translate(input, ...args);
}
function tn(key: string, ...args: unknown[]) {
if (!isConfigured) warnPreConfigure('tn', key);
return getInstance().translateWithNumber(key, ...args);
}
@@ -16,26 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
export {}; // ensure this file is treated as a module so top-level declarations don't leak into global scope
type LoggingModule = typeof import('./index');
const loadLogging = (): LoggingModule['logging'] => {
let logging: LoggingModule['logging'] | undefined;
jest.isolateModules(() => {
({ logging } = jest.requireActual<LoggingModule>(
'@apache-superset/core/utils',
));
});
return logging!;
};
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
});
test('should pipe to `console` methods', () => {
const logging = loadLogging();
const { logging } = require('@apache-superset/core/utils');
jest.spyOn(logging, 'debug').mockImplementation();
jest.spyOn(logging, 'log').mockImplementation();
@@ -63,24 +50,20 @@ test('should pipe to `console` methods', () => {
});
test('should use noop functions when console unavailable', () => {
const originalConsole = window.console;
Object.assign(window, { console: undefined });
try {
const logging = loadLogging();
const { logging } = require('@apache-superset/core/utils');
expect(() => {
logging.debug();
logging.log();
logging.info();
logging.warn('warn');
logging.error('error');
logging.trace();
logging.table([
[1, 2],
[3, 4],
]);
}).not.toThrow();
} finally {
Object.assign(window, { console: originalConsole });
}
expect(() => {
logging.debug();
logging.log();
logging.info();
logging.warn('warn');
logging.error('error');
logging.trace();
logging.table([
[1, 2],
[3, 4],
]);
}).not.toThrow();
Object.assign(window, { console });
});
@@ -36,7 +36,7 @@
*/
import { ReactElement } from 'react';
import { Disposable, Event } from '../common';
import { Disposable } from '../common';
/**
* Represents a contributed view in the application.
@@ -88,33 +88,3 @@ export declare function registerView(
* ```
*/
export declare function getViews(location: string): View[] | undefined;
/**
* Event fired when a view is registered.
*/
export interface ViewRegisteredEvent {
/** The descriptor of the view that was registered. */
view: View;
/** The location where the view was registered. */
location: string;
}
/**
* Event fired when a view is unregistered.
*/
export interface ViewUnregisteredEvent {
/** The descriptor of the view that was unregistered. */
view: View;
/** The location where the view was registered. */
location: string;
}
/**
* Event fired when a view is registered.
*/
export declare const onDidRegisterView: Event<ViewRegisteredEvent>;
/**
* Event fired when a view is unregistered.
*/
export declare const onDidUnregisterView: Event<ViewUnregisteredEvent>;
@@ -40,9 +40,9 @@
"ace-builds": "^1.4.14",
"brace": "^0.11.1",
"memoize-one": "^5.1.1",
"react": "^18.3.0",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-dom": "^18.3.0"
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"
@@ -204,14 +204,8 @@ export type TabOverride = 'data' | 'customize' | 'matrixify' | boolean;
* these configs will be passed to the UI component for control as props.
*
* - type: the control type, referencing a React component of the same name
* - label: the label as shown in the control's header. When the value involves
* `t()`/`tn()`, prefer the arrow-function form (`label: () => t('Foo')`) so
* the lookup runs at render time rather than at module load eager
* `label: t('Foo')` captures the fallback language before i18n initializes
* and does not update on runtime language change. The
* `i18n-strings/no-eager-t-in-config` lint rule autofixes this.
* - description: shown in the info tooltip of the control's header. Same
* lazy-form guidance as `label`.
* - label: the label as shown in the control's header
* - description: shown in the info tooltip of the control's header
* - default: the default value when opening a new chart, or changing visualization type
* - renderTrigger: a bool that defines whether the visualization should be re-rendered
* when changed. This should `true` for controls that only affect the rendering (client side)
@@ -43,7 +43,7 @@
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.21",
"dompurify": "^3.4.9",
"dompurify": "^3.4.7",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
"jed": "^1.1.1",
@@ -75,9 +75,9 @@
"@types/d3-scale": "^2.1.1",
"@types/d3-time": "^3.0.4",
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.1",
"@types/jquery": "^4.0.0",
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.2",
"@types/node": "^25.9.1",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -101,8 +101,8 @@
"@types/tinycolor2": "*",
"antd": "^5.26.0",
"nanoid": "^5.0.9",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*"
},
@@ -72,7 +72,6 @@ test('should generate a 2x2 grid for metrics mode', () => {
createAdhocMetric('Revenue'),
createSqlMetric('Q1', 'SUM(CASE WHEN quarter = 1 THEN value END)'),
]);
expect(firstCell!.formData.metric).toEqual(createAdhocMetric('Revenue'));
});
test('should generate grid for dimensions mode', () => {
@@ -214,9 +213,6 @@ test('should skip missing column metrics when generating cell form data', () =>
expect(grid!.cells[0][0]!.formData.metrics).toEqual([
createAdhocMetric('Revenue'),
]);
expect(grid!.cells[0][0]!.formData.metric).toEqual(
createAdhocMetric('Revenue'),
);
});
test('should not escape HTML entities in cell titles', () => {
@@ -475,51 +471,6 @@ test('should handle metrics without labels', () => {
expect(grid!.colHeaders).toEqual(['count']);
});
test('should set singular metric for singular-metric chart types like Pie', () => {
const rowMetricFormData: TestFormData = {
viz_type: 'pie',
datasource: '1__table',
matrixify_enable: true,
matrixify_mode_rows: 'metrics',
matrixify_rows: [createAdhocMetric('Revenue'), createAdhocMetric('Profit')],
};
const grid = generateMatrixifyGrid(rowMetricFormData);
expect(grid).not.toBeNull();
expect(grid!.cells[0][0]!.formData.metrics).toEqual([
createAdhocMetric('Revenue'),
]);
expect(grid!.cells[0][0]!.formData.metric).toEqual(
createAdhocMetric('Revenue'),
);
expect(grid!.cells[1][0]!.formData.metrics).toEqual([
createAdhocMetric('Profit'),
]);
expect(grid!.cells[1][0]!.formData.metric).toEqual(
createAdhocMetric('Profit'),
);
});
test('should not overwrite singular metric in dimension-only mode', () => {
const dimensionFormData: TestFormData = {
viz_type: 'pie',
datasource: '1__table',
matrixify_enable: true,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_rows: {
dimension: 'country',
values: ['USA', 'Canada'],
},
metric: 'existing_metric',
};
const grid = generateMatrixifyGrid(dimensionFormData);
expect(grid).not.toBeNull();
expect(grid!.cells[0][0]!.formData.metric).toBe('existing_metric');
});
test('should preserve slice_id and dashboardId for embedded dashboard permissions', () => {
const formDataWithDashboardContext: TestFormData = {
...baseFormData,
@@ -197,7 +197,6 @@ function generateCellFormData(
// If we have metrics from the matrix, use them; otherwise keep original
if (metrics.length > 0) {
cellFormData.metrics = metrics;
cellFormData.metric = metrics[0];
}
return cellFormData;
@@ -18,7 +18,6 @@
*/
import { createRef } from 'react';
import { render, screen, waitFor } from '@superset-ui/core/spec';
import { supersetTheme } from '@apache-superset/core/theme';
import type AceEditor from 'react-ace';
import {
AsyncAceEditor,
@@ -29,7 +28,6 @@ import {
CssEditor,
JsonEditor,
ConfigEditor,
aceCompletionHighlightStyles,
} from '.';
import type { AceModule, AsyncAceEditorOptions } from './types';
@@ -44,17 +42,6 @@ test('renders SQLEditor', async () => {
});
});
test('themes the autocomplete completion highlight from the theme', () => {
// Ace ships a hardcoded `color: #000` for the matched-prefix highlight, which
// is invisible on the dark autocomplete popup. The shared editor overrides it
// from the theme so every Ace editor (SQL Lab, Explore Custom SQL, ...) stays
// consistent.
const { styles } = aceCompletionHighlightStyles(supersetTheme);
expect(styles).toContain('.ace_completion-highlight');
expect(styles).toContain(supersetTheme.colorPrimaryText);
});
test('SQLEditor uses fontFamilyCode from theme', async () => {
const ref = createRef<AceEditor>();
const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />);
@@ -32,7 +32,7 @@ import {
AsyncEsmComponent,
PlaceholderProps,
} from '@superset-ui/core/components/AsyncEsmComponent';
import { useTheme, css, type SupersetTheme } from '@apache-superset/core/theme';
import { useTheme, css } from '@apache-superset/core/theme';
import { Global } from '@emotion/react';
export { getTooltipHTML } from './Tooltip';
@@ -105,19 +105,6 @@ export type AsyncAceEditorOptions = {
> | null;
};
/**
* Theme-aware styling for the matched-prefix highlight in the autocomplete
* popup. Ace ships a hardcoded `color: #000` that is invisible on the dark
* popup, so the override needs `!important` to win. Lives in the shared editor
* so every Ace editor (SQL Lab, Explore Custom SQL, ...) stays consistent.
*/
export const aceCompletionHighlightStyles = (token: SupersetTheme) => css`
.ace_completion-highlight {
color: ${token.colorPrimaryText} !important;
background-color: ${token.colorPrimaryBgHover};
}
`;
/**
* Get an async AceEditor with automatical loading of specified ace modules.
*/
@@ -383,8 +370,6 @@ export function AsyncAceEditor(
display: flex !important;
}
${aceCompletionHighlightStyles(token)}
&&& .tooltip-detail {
display: flex;
justify-content: center;
@@ -17,23 +17,15 @@
* under the License.
*/
import { forwardRef } from 'react';
import { Avatar as AntdAvatar } from 'antd';
import type { AvatarProps, GroupProps as AvatarGroupProps } from './types';
export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>((props, ref) => (
<AntdAvatar ref={ref} {...props} />
));
export function Avatar(props: AvatarProps) {
return <AntdAvatar {...props} />;
}
// antd Avatar.Group is a plain function component without forwardRef; wrap in
// a span so this component can be a Tooltip / Popover trigger and skip the
// findDOMNode fallback.
export const AvatarGroup = forwardRef<HTMLSpanElement, AvatarGroupProps>(
(props, ref) => (
<span ref={ref}>
<AntdAvatar.Group {...props} />
</span>
),
);
export function AvatarGroup(props: AvatarGroupProps) {
return <AntdAvatar.Group {...props} />;
}
export type { AvatarProps, AvatarGroupProps };
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Children, ReactElement, Fragment, forwardRef, Ref } from 'react';
import { Children, ReactElement, Fragment } from 'react';
import cx from 'classnames';
import { Button as AntdButton } from 'antd';
import { useTheme } from '@apache-superset/core/theme';
@@ -100,7 +100,7 @@ const BUTTON_STYLE_MAP: Record<
link: { type: 'link' },
};
function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
export function Button(props: ButtonProps) {
const {
tooltip,
placement,
@@ -160,7 +160,6 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
const button = (
<AntdButton
ref={ref as Ref<HTMLButtonElement & HTMLAnchorElement>}
href={disabled ? undefined : href}
disabled={disabled}
type={antdType}
@@ -236,6 +235,4 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
return button;
}
export const Button = forwardRef<HTMLElement, ButtonProps>(ButtonInner);
export type { ButtonProps, OnClickHandler };
@@ -75,10 +75,7 @@ export const DropdownButton = ({
id={`${kebabCase(tooltip)}-tooltip`}
title={tooltip}
>
{/* antd Dropdown.Button is a plain function component without
forwardRef; wrap in a span so the Tooltip can attach a ref to a
real DOM node and skip the findDOMNode fallback. */}
<span>{button}</span>
{button}
</Tooltip>
);
}
@@ -70,46 +70,11 @@ test('a change event that arrives before isEditing flips is not dropped', () =>
});
test('prop changes mid-edit do not clobber unsaved typing', async () => {
// Rerender DynamicEditableTitle directly with a changed title prop so the
// sync effect actually runs. Going through Harness would not exercise the
// bug because Harness owns its own state and only reads initialTitle once.
const onSave = jest.fn();
const props = {
placeholder: 'placeholder',
canEdit: true,
label: 'Title',
onSave,
};
const { rerender } = render(<DynamicEditableTitle {...props} title="Foo" />);
const { rerender } = render(<Harness initialTitle="Foo" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
await userEvent.type(input, 'X', { delay: 1 });
expect(input.value).toBe('FooX');
rerender(<DynamicEditableTitle {...props} title="Bar" />);
rerender(<Harness initialTitle="Foo" />);
expect(input.value).toBe('FooX');
// Locks in commit semantics: blur after a real edit must persist the
// user's typed value, even when a competing parent-driven title arrived
// mid-edit.
fireEvent.blur(input);
expect(onSave).toHaveBeenCalledWith('FooX');
});
test('passive focus then parent-driven title change then blur does not revert', () => {
// Phantom-revert scenario: user clicks the input but does not type, the
// parent autosaves a new title from elsewhere, then the user blurs. The
// component must NOT call onSave with the stale local value, otherwise it
// would silently overwrite the parent's update.
const onSave = jest.fn();
const props = {
placeholder: 'placeholder',
canEdit: true,
label: 'Title',
onSave,
};
const { rerender } = render(<DynamicEditableTitle {...props} title="Foo" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
rerender(<DynamicEditableTitle {...props} title="Bar" />);
fireEvent.blur(input);
expect(onSave).not.toHaveBeenCalled();
});
@@ -81,25 +81,12 @@ export const DynamicEditableTitle = memo(
const sizerRef = useRef<HTMLSpanElement>(null);
const inputRef = useRef<InputRef>(null);
// Tracks whether the user has actually typed since entering edit mode.
// Gates onSave so that passive focus (click without typing) followed by a
// parent-driven title change and blur does not silently revert the
// parent's update with our stale currentTitle.
const dirtyRef = useRef(false);
const { width: containerWidth, ref: containerRef } = useResizeDetector({
refreshMode: 'debounce',
});
useEffect(() => {
// Don't overwrite in-flight user input when the parent re-renders with a
// new title prop mid-edit. handleBlur already syncs currentTitle on commit;
// re-running this effect when isEditing flips would resync to a stale
// title prop, so isEditing is intentionally read via closure rather than
// listed as a dep.
if (!isEditing) {
setCurrentTitle(title);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
setCurrentTitle(title);
}, [title]);
useEffect(() => {
if (isEditing) {
@@ -151,19 +138,10 @@ export const DynamicEditableTitle = memo(
return;
}
const formattedTitle = currentTitle.trim();
// Only commit when the user actually typed. Passive focus must not
// overwrite a parent-driven title change that landed mid-edit.
if (dirtyRef.current && title !== formattedTitle) {
setCurrentTitle(formattedTitle);
setCurrentTitle(formattedTitle);
if (title !== formattedTitle) {
onSave(formattedTitle);
} else if (!dirtyRef.current) {
// Drop any stale local state and resync to the latest title prop so a
// subsequent edit starts from the current parent value.
setCurrentTitle(title);
} else {
setCurrentTitle(formattedTitle);
}
dirtyRef.current = false;
setIsEditing(false);
}, [canEdit, currentTitle, onSave, title]);
@@ -180,7 +158,6 @@ export const DynamicEditableTitle = memo(
if (!isEditing) {
setIsEditing(true);
}
dirtyRef.current = true;
setCurrentTitle(ev.target.value);
},
[canEdit, isEditing],
@@ -240,10 +240,7 @@ export function EditableTitle({
t("You don't have the rights to alter this title.")
}
>
{/* Wrap in span so the Tooltip can attach a ref to a DOM element.
antd's Input.TextArea forwards a non-DOM imperative handle, which
triggers a React 18 findDOMNode deprecation warning. */}
<span>{titleComponent}</span>
{titleComponent}
</Tooltip>
);
}
@@ -16,54 +16,47 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef } from 'react';
import { Tooltip } from '../Tooltip';
import { Button } from '../Button';
import type { IconTooltipProps } from './types';
export const IconTooltip = forwardRef<HTMLElement, IconTooltipProps>(
(
{
children = null,
className = '',
onClick = () => undefined,
placement = 'top',
style = {},
tooltip = null,
mouseEnterDelay = 0.3,
mouseLeaveDelay = 0.15,
},
ref,
) => {
const iconTooltip = (
<Button
ref={ref}
onClick={onClick}
style={{
padding: 0,
...style,
}}
buttonStyle="link"
className={`IconTooltip ${className}`}
export const IconTooltip = ({
children = null,
className = '',
onClick = () => undefined,
placement = 'top',
style = {},
tooltip = null,
mouseEnterDelay = 0.3,
mouseLeaveDelay = 0.15,
}: IconTooltipProps) => {
const iconTooltip = (
<Button
onClick={onClick}
style={{
padding: 0,
...style,
}}
buttonStyle="link"
className={`IconTooltip ${className}`}
>
{children}
</Button>
);
if (tooltip) {
return (
<Tooltip
id="tooltip"
title={tooltip}
placement={placement}
mouseEnterDelay={mouseEnterDelay}
mouseLeaveDelay={mouseLeaveDelay}
>
{children}
</Button>
{iconTooltip}
</Tooltip>
);
if (tooltip) {
return (
<Tooltip
id="tooltip"
title={tooltip}
placement={placement}
mouseEnterDelay={mouseEnterDelay}
mouseLeaveDelay={mouseLeaveDelay}
>
{iconTooltip}
</Tooltip>
);
}
return iconTooltip;
},
);
}
return iconTooltip;
};
export type { IconTooltipProps };
@@ -165,7 +165,7 @@ import {
SlackOutlined,
ApiOutlined,
} from '@ant-design/icons';
import { ForwardRefExoticComponent, RefAttributes, forwardRef } from 'react';
import { FC } from 'react';
import { IconType } from './types';
import { BaseIconComponent } from './BaseIcon';
@@ -323,25 +323,19 @@ type AntdIconNames = keyof typeof AntdIcons;
export const antdEnhancedIcons: Record<
AntdIconNames,
ForwardRefExoticComponent<IconType & RefAttributes<HTMLSpanElement>>
FC<IconType>
> = Object.keys(AntdIcons)
.filter(key => !EXCLUDED_ICONS.some(excluded => key.includes(excluded)))
.reduce(
(acc, key) => {
acc[key as AntdIconNames] = forwardRef<HTMLSpanElement, IconType>(
(props, ref) => (
<BaseIconComponent
ref={ref}
component={AntdIcons[key as AntdIconNames]}
fileName={key}
{...props}
/>
),
acc[key as AntdIconNames] = (props: IconType) => (
<BaseIconComponent
component={AntdIcons[key as AntdIconNames]}
fileName={key}
{...props}
/>
);
return acc;
},
{} as Record<
AntdIconNames,
ForwardRefExoticComponent<IconType & RefAttributes<HTMLSpanElement>>
>,
{} as Record<AntdIconNames, FC<IconType>>,
);
@@ -17,12 +17,12 @@
* under the License.
*/
import { FC, SVGProps, forwardRef, useEffect, useRef, useState } from 'react';
import { FC, SVGProps, useEffect, useRef, useState } from 'react';
import TransparentIcon from './svgs/transparent.svg';
import { IconType } from './types';
import { BaseIconComponent } from './BaseIcon';
const AsyncIcon = forwardRef<HTMLSpanElement, IconType>((props, ref) => {
const AsyncIcon = (props: IconType) => {
const [, setLoaded] = useState(false);
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
const { fileName, customIcons, iconSize, iconColor, viewBox, ...restProps } =
@@ -46,7 +46,6 @@ const AsyncIcon = forwardRef<HTMLSpanElement, IconType>((props, ref) => {
return (
<BaseIconComponent
ref={ref}
component={ImportedSVG.current || TransparentIcon}
fileName={fileName}
customIcons={customIcons}
@@ -56,6 +55,6 @@ const AsyncIcon = forwardRef<HTMLSpanElement, IconType>((props, ref) => {
{...restProps}
/>
);
});
};
export default AsyncIcon;
@@ -17,7 +17,6 @@
* under the License.
*/
import { forwardRef, type ComponentType } from 'react';
import { css, useTheme, getFontSize } from '@apache-superset/core/theme';
import { AntdIconType, BaseIconProps, CustomIconType, IconType } from './types';
@@ -36,78 +35,65 @@ const genAriaLabel = (fileName: string) => {
return name.toLowerCase();
};
export const BaseIconComponent = forwardRef<
HTMLSpanElement | SVGSVGElement,
export const BaseIconComponent: React.FC<
BaseIconProps & Omit<IconType, 'component'>
>(
(
{
component: Component,
iconColor,
iconSize,
viewBox,
customIcons,
fileName,
...rest
},
ref,
) => {
const theme = useTheme();
const whatRole = rest?.onClick ? 'button' : 'img';
const ariaLabel = genAriaLabel(fileName || '');
const style = {
color: iconColor,
fontSize: iconSize
? `${getFontSize(theme, iconSize)}px`
: `${theme.fontSize}px`,
cursor: rest?.onClick ? 'pointer' : undefined,
};
> = ({
component: Component,
iconColor,
iconSize,
viewBox,
customIcons,
fileName,
...rest
}) => {
const theme = useTheme();
const whatRole = rest?.onClick ? 'button' : 'img';
const ariaLabel = genAriaLabel(fileName || '');
const style = {
color: iconColor,
fontSize: iconSize
? `${getFontSize(theme, iconSize)}px`
: `${theme.fontSize}px`,
cursor: rest?.onClick ? 'pointer' : undefined,
};
const AntdComponent = Component as ComponentType<
Record<string, unknown> & {
ref?: React.Ref<HTMLSpanElement | SVGSVGElement>;
}
>;
return customIcons ? (
<span
ref={ref as React.Ref<HTMLSpanElement>}
role={whatRole}
aria-label={ariaLabel}
data-test={ariaLabel}
css={[
css`
display: inline-flex;
align-items: center;
line-height: 0;
vertical-align: middle;
`,
]}
>
<Component
viewBox={viewBox || '0 0 24 24'}
style={style}
width={
iconSize
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
: `${theme.fontSize}px`
}
height={
iconSize
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
: `${theme.fontSize}px`
}
{...(rest as CustomIconType)}
/>
</span>
) : (
<AntdComponent
ref={ref}
role={whatRole}
return customIcons ? (
<span
role={whatRole}
aria-label={ariaLabel}
data-test={ariaLabel}
css={[
css`
display: inline-flex;
align-items: center;
line-height: 0;
vertical-align: middle;
`,
]}
>
<Component
viewBox={viewBox || '0 0 24 24'}
style={style}
aria-label={ariaLabel}
data-test={ariaLabel}
{...(rest as AntdIconType)}
width={
iconSize
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
: `${theme.fontSize}px`
}
height={
iconSize
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
: `${theme.fontSize}px`
}
{...(rest as CustomIconType)}
/>
);
},
);
</span>
) : (
<Component
role={whatRole}
style={style}
aria-label={ariaLabel}
data-test={ariaLabel}
{...(rest as AntdIconType)}
/>
);
};
@@ -17,16 +17,12 @@
* under the License.
*/
import { ForwardRefExoticComponent, RefAttributes, forwardRef } from 'react';
import { FC } from 'react';
import { antdEnhancedIcons } from './AntdEnhanced';
import AsyncIcon from './AsyncIcon';
import type { IconType } from './types';
type IconComponent = ForwardRefExoticComponent<
IconType & RefAttributes<HTMLSpanElement>
>;
/**
* Filename is going to be inferred from the icon name.
* i.e. BigNumberChartTile => assets/images/icons/big_number_chart_tile
@@ -62,17 +58,15 @@ const customIcons = [
'Undo',
] as const;
type CustomIconType = Record<(typeof customIcons)[number], IconComponent>;
type CustomIconType = Record<(typeof customIcons)[number], FC<IconType>>;
const iconOverrides: CustomIconType = {} as CustomIconType;
customIcons.forEach(customIcon => {
const fileName = customIcon
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
.toLowerCase();
iconOverrides[customIcon] = forwardRef<HTMLSpanElement, IconType>(
(props, ref) => (
<AsyncIcon ref={ref} customIcons fileName={fileName} {...props} />
),
iconOverrides[customIcon] = (props: IconType) => (
<AsyncIcon customIcons fileName={fileName} {...props} />
);
});
@@ -80,7 +74,7 @@ export type IconNameType =
| keyof typeof antdEnhancedIcons
| keyof typeof iconOverrides;
type IconComponentType = Record<IconNameType, IconComponent>;
type IconComponentType = Record<IconNameType, FC<IconType>>;
export const Icons: IconComponentType = {
...antdEnhancedIcons,
@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef } from 'react';
import { Tag } from '@superset-ui/core/components/Tag';
import { css } from '@emotion/react';
import { useTheme, getColorVariants } from '@apache-superset/core/theme';
@@ -24,7 +23,7 @@ import { DatasetTypeLabel } from './reusable/DatasetTypeLabel';
import { PublishedLabel } from './reusable/PublishedLabel';
import type { LabelProps } from './types';
export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
export function Label(props: LabelProps) {
const theme = useTheme();
// Use Ant Design's motion duration instead of deprecated transitionTiming
const {
@@ -72,7 +71,6 @@ export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
return (
<Tag
ref={ref}
onClick={onClick}
role={onClick ? 'button' : undefined}
style={style}
@@ -83,6 +81,6 @@ export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
{children}
</Tag>
);
});
}
export { DatasetTypeLabel, PublishedLabel };
export type { LabelType } from './types';
@@ -371,9 +371,6 @@ const CustomModal = ({
disabled={!draggable || dragDisabled}
bounds={bounds ?? false}
onStart={(event, uiData) => onDragStart(event, uiData)}
// Pass nodeRef so react-draggable does not fall back to
// ReactDOM.findDOMNode (deprecated in React 18+ Strict Mode).
nodeRef={draggableRef}
{...draggableConfig}
>
{resizable ? (
@@ -16,15 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef } from 'react';
import { Popover as AntdPopover } from 'antd';
import { PopoverProps as AntdPopoverProps } from 'antd/es/popover';
import type { TooltipRef } from 'antd/es/tooltip';
export interface PopoverProps extends AntdPopoverProps {
forceRender?: boolean;
}
export const Popover = forwardRef<TooltipRef, PopoverProps>((props, ref) => (
<AntdPopover ref={ref} {...props} />
));
export const Popover = (props: PopoverProps) => <AntdPopover {...props} />;
@@ -16,9 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { MouseEventHandler } from 'react';
import { MouseEventHandler, forwardRef } from 'react';
import { SupersetTheme } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
import type { IconType } from '@superset-ui/core/components/Icons/types';
import { Tooltip } from '../Tooltip';
export interface RefreshLabelProps {
@@ -31,19 +32,25 @@ const RefreshLabel = ({
onClick,
tooltipContent,
disabled,
}: RefreshLabelProps) => (
<Tooltip title={tooltipContent}>
<Icons.SyncOutlined
iconSize="l"
role="button"
onClick={disabled ? undefined : onClick}
css={(theme: SupersetTheme) => ({
cursor: 'pointer',
color: theme.colorIcon,
'&:hover': { color: theme.colorPrimary },
})}
/>
</Tooltip>
);
}: RefreshLabelProps) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const IconWithoutRef = forwardRef((props: IconType, ref: any) => (
<Icons.SyncOutlined iconSize="l" {...props} />
));
return (
<Tooltip title={tooltipContent}>
<IconWithoutRef
role="button"
onClick={disabled ? undefined : onClick}
css={(theme: SupersetTheme) => ({
cursor: 'pointer',
color: theme.colorIcon,
'&:hover': { color: theme.colorPrimary },
})}
/>
</Tooltip>
);
};
export default RefreshLabel;
@@ -16,22 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef } from 'react';
import { Tooltip as AntdTooltip } from 'antd';
import type { TooltipRef } from 'antd/es/tooltip';
import type { TooltipProps, TooltipPlacement } from './types';
export const Tooltip = forwardRef<TooltipRef, TooltipProps>(
({ overlayStyle, ...props }, ref) => (
<AntdTooltip
ref={ref}
styles={{
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
root: overlayStyle ?? {},
}}
{...props}
/>
),
export const Tooltip = ({ overlayStyle, ...props }: TooltipProps) => (
<AntdTooltip
styles={{
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
root: overlayStyle ?? {},
}}
{...props}
/>
);
export type { TooltipProps, TooltipPlacement };
@@ -35,4 +35,3 @@ export * from './typedMemo';
export * from './html';
export * from './tooltip';
export * from './merge';
export * from './mapStyles';
@@ -1,295 +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 {
getBootstrapDataFromDocument,
getDefaultMapRenderer,
getMapProviderMapStyle,
getMapboxApiKeyFromBootstrap,
getMapRendererOptions,
hasMapboxApiKey,
isRasterTileTemplate,
OSM_TILE_ATTRIBUTION,
OSM_TILE_STYLE_URL,
resolveMapStyle,
} from './mapStyles';
test('OSM style metadata uses the approved URL and attribution', () => {
expect(OSM_TILE_STYLE_URL).toBe(
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
);
expect(OSM_TILE_ATTRIBUTION).toBe('© OpenStreetMap contributors');
});
test('Mapbox key helpers report absence and presence from bootstrap data', () => {
expect(getMapboxApiKeyFromBootstrap({ common: { conf: {} } })).toBe('');
expect(hasMapboxApiKey({ common: { conf: {} } })).toBe(false);
expect(
getMapboxApiKeyFromBootstrap({
common: { conf: { MAPBOX_API_KEY: 'pk.test' } },
}),
).toBe('pk.test');
expect(
getMapboxApiKeyFromBootstrap({
common: { conf: { MAPBOX_API_KEY: ' pk.test ' } },
}),
).toBe('pk.test');
expect(hasMapboxApiKey({ common: { conf: { MAPBOX_API_KEY: ' ' } } })).toBe(
false,
);
expect(
hasMapboxApiKey({ common: { conf: { MAPBOX_API_KEY: 'pk.test' } } }),
).toBe(true);
});
test('bootstrap data helper parses document data safely', () => {
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify({
common: { conf: { MAPBOX_API_KEY: 'pk.document' } },
})}'></div>`;
expect(getBootstrapDataFromDocument()).toEqual({
common: { conf: { MAPBOX_API_KEY: 'pk.document' } },
});
document.body.innerHTML = `<div id="app" data-bootstrap='not-json'></div>`;
expect(getBootstrapDataFromDocument()).toBeUndefined();
document.body.innerHTML = '';
expect(getBootstrapDataFromDocument()).toBeUndefined();
});
test('bootstrap data helper returns undefined without a document', () => {
// jsdom defines `document` as a non-configurable global, so the SSR guard
// cannot be exercised by deleting it. Instead, re-evaluate the function's
// own source in a scope where `document` is shadowed with undefined. When
// running under coverage, the source is istanbul-instrumented and references
// its module-scoped counter, so the counter is injected to keep the guard's
// execution attributed to mapStyles.ts.
const source = getBootstrapDataFromDocument.toString();
const counterName = source.match(/cov_\w+/)?.[0] ?? 'unusedCoverageCounter';
const coverage = (globalThis as { __coverage__?: Record<string, unknown> })
.__coverage__;
const coverageEntry =
coverage?.[
Object.keys(coverage).find(file => file.endsWith('mapStyles.ts')) ?? ''
];
// eslint-disable-next-line no-new-func
const callWithoutDocument = new Function(
counterName,
'document',
`return (${source})();`,
);
expect(callWithoutDocument(() => coverageEntry, undefined)).toBeUndefined();
});
test('renderer options enable Mapbox only when a key is available', () => {
expect(getMapRendererOptions({ hasMapboxKey: true })).toEqual([
{ value: 'maplibre' },
{ value: 'mapbox' },
]);
expect(getMapRendererOptions({ hasMapboxKey: false })).toEqual([
{ value: 'maplibre' },
]);
});
test('renderer options preserve saved Mapbox without API-key labels', () => {
expect(
getMapRendererOptions({ hasMapboxKey: false, currentValue: 'mapbox' }),
).toEqual([{ value: 'maplibre' }, { value: 'mapbox', disabled: true }]);
});
test('map provider style helper preserves legacy non-Mapbox styles for MapLibre', () => {
expect(
getMapProviderMapStyle({
mapProvider: 'maplibre',
maplibreStyle: undefined,
mapboxStyle: OSM_TILE_STYLE_URL,
legacyMapStyle: 'https://example.com/fallback-style.json',
}),
).toEqual({
mapProvider: 'maplibre',
mapStyle: OSM_TILE_STYLE_URL,
});
});
test('map provider style helper does not send Mapbox URLs to MapLibre', () => {
expect(
getMapProviderMapStyle({
mapProvider: 'maplibre',
mapboxStyle: 'mapbox://styles/mapbox/dark-v11',
legacyMapStyle: 'https://example.com/fallback-style.json',
}),
).toEqual({
mapProvider: 'maplibre',
mapStyle: 'https://example.com/fallback-style.json',
});
});
test('map provider style helper uses Mapbox style when Mapbox is selected', () => {
expect(
getMapProviderMapStyle({
mapProvider: 'mapbox',
mapboxStyle: 'mapbox://styles/mapbox/dark-v11',
legacyMapStyle: 'https://example.com/fallback-style.json',
}),
).toEqual({
mapProvider: 'mapbox',
mapStyle: 'mapbox://styles/mapbox/dark-v11',
});
});
test('default renderer uses configured Mapbox only when a key is available', () => {
expect(
getDefaultMapRenderer({
common: {
conf: {
DEFAULT_MAP_RENDERER: 'mapbox',
MAPBOX_API_KEY: 'pk.test',
},
},
}),
).toBe('mapbox');
expect(
getDefaultMapRenderer({
common: { conf: { DEFAULT_MAP_RENDERER: 'mapbox' } },
}),
).toBe('maplibre');
expect(
getDefaultMapRenderer({
common: {
conf: {
DEFAULT_MAP_RENDERER: 'invalid',
MAPBOX_API_KEY: 'pk.test',
},
},
}),
).toBe('maplibre');
});
test('raster tile templates resolve to MapLibre raster style objects with attribution', () => {
const style = resolveMapStyle(OSM_TILE_STYLE_URL, 'default-style.json');
expect(style).toEqual({
version: 8,
sources: {
'osm-raster-tiles': {
type: 'raster',
tiles: [OSM_TILE_STYLE_URL],
tileSize: 256,
attribution: OSM_TILE_ATTRIBUTION,
},
},
layers: [
{
id: 'osm-raster-layer',
type: 'raster',
source: 'osm-raster-tiles',
minzoom: 0,
maxzoom: 22,
},
],
});
});
test('tile protocol raster templates are unwrapped before style resolution', () => {
const style = resolveMapStyle(
`tile://${OSM_TILE_STYLE_URL}`,
'default-style.json',
);
expect(typeof style).toBe('object');
if (typeof style !== 'string') {
expect(style.sources['osm-raster-tiles'].tiles).toEqual([
OSM_TILE_STYLE_URL,
]);
expect(style.sources['osm-raster-tiles'].attribution).toBe(
OSM_TILE_ATTRIBUTION,
);
}
});
test('OpenStreetMap subdomain raster templates receive OSM attribution', () => {
const osmSubdomainTileUrl =
'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png';
const style = resolveMapStyle(
`tile://${osmSubdomainTileUrl}`,
'default-style.json',
);
expect(typeof style).toBe('object');
if (typeof style !== 'string') {
expect(style.sources['osm-raster-tiles'].tiles).toEqual([
osmSubdomainTileUrl,
]);
expect(style.sources['osm-raster-tiles'].attribution).toBe(
OSM_TILE_ATTRIBUTION,
);
}
});
test('custom raster tile templates do not receive OSM attribution', () => {
const customTileUrl = 'https://tiles.example.com/{z}/{x}/{y}.png';
const style = resolveMapStyle(
`tile://${customTileUrl}`,
'default-style.json',
);
expect(typeof style).toBe('object');
if (typeof style !== 'string') {
expect(style.sources['osm-raster-tiles'].tiles).toEqual([customTileUrl]);
expect(style.sources['osm-raster-tiles']).not.toHaveProperty('attribution');
}
});
test('relative raster tile templates do not receive OSM attribution', () => {
const relativeTileUrl = '/tiles/{z}/{x}/{y}.png';
const style = resolveMapStyle(relativeTileUrl, 'default-style.json');
expect(typeof style).toBe('object');
if (typeof style !== 'string') {
expect(style.sources['osm-raster-tiles'].tiles).toEqual([relativeTileUrl]);
expect(style.sources['osm-raster-tiles']).not.toHaveProperty('attribution');
}
});
test('lookalike OpenStreetMap hostnames do not receive OSM attribution', () => {
const lookalikeTileUrl =
'https://openstreetmap.org.example.com/{z}/{x}/{y}.png';
const style = resolveMapStyle(
`tile://${lookalikeTileUrl}`,
'default-style.json',
);
expect(typeof style).toBe('object');
if (typeof style !== 'string') {
expect(style.sources['osm-raster-tiles'].tiles).toEqual([lookalikeTileUrl]);
expect(style.sources['osm-raster-tiles']).not.toHaveProperty('attribution');
}
});
test('style JSON URLs pass through without raster wrapping', () => {
const styleUrl = 'https://example.com/styles/custom-style.json';
expect(isRasterTileTemplate(undefined)).toBe(false);
expect(isRasterTileTemplate(styleUrl)).toBe(false);
expect(resolveMapStyle(styleUrl, 'default-style.json')).toBe(styleUrl);
expect(resolveMapStyle(undefined, 'default-style.json')).toBe(
'default-style.json',
);
});
@@ -1,251 +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.
*/
export type MapProvider = 'maplibre' | 'mapbox';
export type MapRendererOption = {
value: MapProvider;
disabled?: boolean;
};
export type MapProviderMapStyle = {
mapProvider?: unknown;
maplibreStyle?: unknown;
mapboxStyle?: unknown;
legacyMapStyle?: unknown;
};
export type SelectedMapProviderMapStyle = {
mapProvider: MapProvider;
mapStyle?: string;
};
export type RasterTileMapStyle = {
version: 8;
sources: {
[sourceId: string]: {
type: 'raster';
tiles: string[];
tileSize: 256;
attribution?: string;
};
};
layers: [
{
id: string;
type: 'raster';
source: string;
minzoom: 0;
maxzoom: 22;
},
];
};
export type ResolvedMapStyle = string | RasterTileMapStyle;
export const OSM_TILE_STYLE_URL =
'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
export const OSM_TILE_ATTRIBUTION = '© OpenStreetMap contributors';
export const MAPLIBRE_RENDERER_OPTION: MapRendererOption = {
value: 'maplibre',
};
export const MAPBOX_RENDERER_OPTION: MapRendererOption = {
value: 'mapbox',
};
export const DISABLED_MAPBOX_RENDERER_OPTION: MapRendererOption = {
...MAPBOX_RENDERER_OPTION,
disabled: true,
};
const TILE_PROTOCOL = 'tile://';
const RASTER_SOURCE_ID = 'osm-raster-tiles';
const RASTER_LAYER_ID = 'osm-raster-layer';
type BootstrapData = {
common?: {
conf?: {
DEFAULT_MAP_RENDERER?: unknown;
MAPBOX_API_KEY?: unknown;
};
};
};
export function getBootstrapDataFromDocument(): unknown {
if (typeof document === 'undefined') {
return undefined;
}
try {
const appContainer = document.getElementById('app');
const dataBootstrap = appContainer?.getAttribute('data-bootstrap');
return dataBootstrap ? JSON.parse(dataBootstrap) : undefined;
} catch {
return undefined;
}
}
export function getMapboxApiKeyFromBootstrap(
bootstrapData: unknown = getBootstrapDataFromDocument(),
): string {
const mapboxApiKey = (bootstrapData as BootstrapData | undefined)?.common
?.conf?.MAPBOX_API_KEY;
return typeof mapboxApiKey === 'string' ? mapboxApiKey.trim() : '';
}
export function hasMapboxApiKey(
bootstrapData: unknown = getBootstrapDataFromDocument(),
): boolean {
return getMapboxApiKeyFromBootstrap(bootstrapData).trim().length > 0;
}
export function getDefaultMapRenderer(
bootstrapData: unknown = getBootstrapDataFromDocument(),
): MapProvider {
const conf = (bootstrapData as BootstrapData | undefined)?.common?.conf;
const defaultRenderer = conf?.DEFAULT_MAP_RENDERER;
if (defaultRenderer === 'mapbox' && hasMapboxApiKey(bootstrapData)) {
return 'mapbox';
}
return 'maplibre';
}
export function getMapRendererOptions({
hasMapboxKey,
currentValue,
}: {
hasMapboxKey: boolean;
currentValue?: MapProvider;
}): MapRendererOption[] {
if (!hasMapboxKey && currentValue !== 'mapbox') {
return [MAPLIBRE_RENDERER_OPTION];
}
return [
MAPLIBRE_RENDERER_OPTION,
hasMapboxKey ? MAPBOX_RENDERER_OPTION : DISABLED_MAPBOX_RENDERER_OPTION,
];
}
function getNonEmptyString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim().length > 0
? value
: undefined;
}
function isMapboxStyle(value: unknown): boolean {
return getNonEmptyString(value)?.startsWith('mapbox://') ?? false;
}
export function getMapProviderMapStyle({
mapProvider,
maplibreStyle,
mapboxStyle,
legacyMapStyle,
}: MapProviderMapStyle): SelectedMapProviderMapStyle {
const selectedMapProvider: MapProvider =
mapProvider === 'mapbox' ? 'mapbox' : 'maplibre';
const maplibreStyleValue = getNonEmptyString(maplibreStyle);
const mapboxStyleValue = getNonEmptyString(mapboxStyle);
const legacyMapStyleValue = getNonEmptyString(legacyMapStyle);
if (selectedMapProvider === 'mapbox') {
return {
mapProvider: selectedMapProvider,
mapStyle: mapboxStyleValue ?? legacyMapStyleValue,
};
}
return {
mapProvider: selectedMapProvider,
mapStyle:
maplibreStyleValue ??
(isMapboxStyle(mapboxStyleValue) ? undefined : mapboxStyleValue) ??
legacyMapStyleValue,
};
}
function unwrapTileProtocol(value: string): string {
return value.startsWith(TILE_PROTOCOL)
? value.slice(TILE_PROTOCOL.length)
: value;
}
export function isRasterTileTemplate(value: unknown): value is string {
if (typeof value !== 'string') {
return false;
}
const tileUrl = unwrapTileProtocol(value);
return ['{z}', '{x}', '{y}'].every(templateParam =>
tileUrl.includes(templateParam),
);
}
function isOpenStreetMapTileUrl(value: string): boolean {
try {
const hostname = new URL(value).hostname.toLowerCase();
return (
hostname === 'openstreetmap.org' ||
hostname.endsWith('.openstreetmap.org')
);
} catch {
return false;
}
}
export function buildRasterTileMapStyle(value: string): RasterTileMapStyle {
const tileUrl = unwrapTileProtocol(value);
const attribution = isOpenStreetMapTileUrl(tileUrl)
? { attribution: OSM_TILE_ATTRIBUTION }
: {};
return {
version: 8,
sources: {
[RASTER_SOURCE_ID]: {
type: 'raster',
tiles: [tileUrl],
tileSize: 256,
...attribution,
},
},
layers: [
{
id: RASTER_LAYER_ID,
type: 'raster',
source: RASTER_SOURCE_ID,
minzoom: 0,
maxzoom: 22,
},
],
};
}
export function resolveMapStyle(
value: string | undefined,
defaultStyle: string,
): ResolvedMapStyle {
if (!value) {
return defaultStyle;
}
return isRasterTileTemplate(value) ? buildRasterTileMapStyle(value) : value;
}
@@ -35,7 +35,7 @@ test('format milliseconds in human readable format with default options', () =>
});
test('format seconds in human readable format with default options', () => {
const formatter = createDurationFormatter({ multiplier: 1000 });
expect(formatter(-0.5)).toBe('0s');
expect(formatter(-0.5)).toBe('-0s');
expect(formatter(0.5)).toBe('0s');
expect(formatter(1)).toBe('1s');
expect(formatter(30)).toBe('30s');
@@ -29,33 +29,12 @@ export class ExplorePage {
private static readonly SELECTORS = {
DATASOURCE_CONTROL: '[data-test="datasource-control"]',
VIZ_SWITCHER: '[data-test="fast-viz-switcher"]',
CHART_CONTAINER: '[data-test="chart-container"]',
} as const;
constructor(page: Page) {
this.page = page;
}
/**
* Navigates to the Explore page for a given chart and waits for it to load.
*
* @param chartId - ID of the chart (slice) to open
* @param options - Optional wait options
*/
async goto(chartId: number, options?: { timeout?: number }): Promise<void> {
await this.page.goto(`explore/?slice_id=${chartId}`);
await this.waitForPageLoad(options);
}
/**
* Gets the chart container locator (where the rendered viz appears).
*
* @returns Locator for the chart container
*/
getChartContainer(): Locator {
return this.page.locator(ExplorePage.SELECTORS.CHART_CONTAINER);
}
/**
* Waits for the Explore page to load.
* Validates URL contains /explore/ and datasource control is visible.
@@ -1,104 +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.
*/
/**
* Regression for #32960: the `formatDate` Handlebars helper (provided by
* just-handlebars-helpers) stopped working after 4.1.2, rendering
* "i is not a function" (minified) / "moment is not a function" (dev) instead
* of the formatted date. The library helper resolves `moment` lazily via
* `global.moment` / `require('moment/min/moment-with-locales')`, which the
* bundled HandlebarsViewer no longer satisfies (it switched to dayjs).
*
* The fix registers a dayjs-backed `formatDate` override in HandlebarsViewer
* (superset-frontend/plugins/plugin-chart-handlebars). This spec guards it: it
* creates a Handlebars chart whose template uses `{{formatDate 'DD.MM.YYYY' ds}}`
* and asserts the chart renders a real formatted date rather than the helper
* error. Because the failure was a bundling/minification artifact (moment
* resolves fine under Jest's Node `require`), an E2E test is required to cover it.
*/
import { testWithAssets, expect } from '../../helpers/fixtures';
import { apiPostChart } from '../../helpers/api/chart';
import { getDatasetByName } from '../../helpers/api/dataset';
import { ExplorePage } from '../../pages/ExplorePage';
import { TIMEOUT } from '../../utils/constants';
const DATASET_NAME = 'birth_names';
testWithAssets(
'Handlebars formatDate helper renders a formatted date (#32960)',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const dataset = await getDatasetByName(page, DATASET_NAME);
if (!dataset) {
throw new Error(`Dataset ${DATASET_NAME} not found`);
}
const datasetId = dataset.id;
const params = {
datasource: `${datasetId}__table`,
viz_type: 'handlebars',
query_mode: 'aggregate',
groupby: ['ds'],
metrics: ['count'],
adhoc_filters: [],
row_limit: 5,
// Note: HTML_SANITIZATION (on by default) strips non-allowlisted
// attributes such as `class`, so the rendered markup is plain
// <ul>/<li> elements. The assertions below target `li` directly.
handlebarsTemplate:
'<ul>{{#each data}}' +
"<li>{{formatDate 'DD.MM.YYYY' ds}}</li>" +
'{{/each}}</ul>',
styleTemplate: '',
};
const chartResp = await apiPostChart(page, {
slice_name: `handlebars_format_date_${Date.now()}`,
viz_type: 'handlebars',
datasource_id: datasetId,
datasource_type: 'table',
params: JSON.stringify(params),
});
expect(chartResp.ok()).toBe(true);
// The chart API may return either a top-level `{ id }` or a wrapped
// `{ result: { id } }` shape; handle both and fail explicitly otherwise.
const chartBody = await chartResp.json();
const chartId: number = chartBody.result?.id ?? chartBody.id;
expect(chartId, 'chart creation should return an id').toBeTruthy();
testAssets.trackChart(chartId);
const explorePage = new ExplorePage(page);
await explorePage.goto(chartId);
const panel = explorePage.getChartContainer();
await panel.waitFor({ state: 'visible', timeout: TIMEOUT.PAGE_LOAD });
// The helper error surfaces as a "... is not a function" message rendered
// in place of the chart content.
await expect(panel).not.toContainText('is not a function', {
timeout: TIMEOUT.API_RESPONSE,
});
// At least one list item should contain a DD.MM.YYYY formatted date.
await expect(panel.locator('li').first()).toHaveText(/\d{2}\.\d{2}\.\d{4}/, {
timeout: TIMEOUT.API_RESPONSE,
});
},
);
@@ -1,202 +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.
*/
/**
* Regression for #28766: a Gauge chart configured with interval bounds and
* interval colors (mapped to a categorical color scheme) sometimes renders the
* wrong interval colors when first loaded on a dashboard a refresh fixes it.
*
* The gauge renders to a <canvas>, so this test reads pixels back from the
* rendered gauge and asserts the configured interval colors are present in the
* correct mapping. With `color_scheme: supersetColors` and
* `interval_color_indices: '1,2'`, the gauge axis must paint the scheme's 1st
* and 2nd colors (#1FA8C9 and #454E7C) and must NOT paint the 3rd (#5AC189),
* which would indicate a shifted / fallback palette.
*
* CI green => the gauge paints the configured interval colors on first load;
* merging closes #28766 and guards against regressions.
* CI red => the interval colors are wrong on first load; the bug is live in
* plugin-chart-echarts/src/Gauge (color-scheme resolution).
*/
import { testWithAssets, expect } from '../../helpers/fixtures';
import { apiPostChart, apiPutChart } from '../../helpers/api/chart';
import { apiPostDashboard } from '../../helpers/api/dashboard';
import { getDatasetByName } from '../../helpers/api/dataset';
import { DashboardPage } from '../../pages/DashboardPage';
const DATASET_NAME = 'birth_names';
// supersetColors palette (1-based, matching interval_color_indices):
// index 1 = #1FA8C9, index 2 = #454E7C, index 3 = #5AC189
const COLOR_INTERVAL_1: [number, number, number] = [31, 168, 201];
const COLOR_INTERVAL_2: [number, number, number] = [69, 78, 124];
const COLOR_UNUSED_3: [number, number, number] = [90, 193, 137];
testWithAssets(
'Gauge renders configured interval colors on a dashboard (#28766)',
async ({ page, testAssets }) => {
const dataset = await getDatasetByName(page, DATASET_NAME);
if (!dataset) {
throw new Error(`Dataset ${DATASET_NAME} not found`);
}
const datasetId = dataset.id;
const sliceName = `gauge_interval_colors_${Date.now()}`;
const chartParams = {
datasource: `${datasetId}__table`,
viz_type: 'gauge_chart',
metric: 'count',
adhoc_filters: [],
row_limit: 10,
color_scheme: 'supersetColors',
min_val: 0,
max_val: 100,
start_angle: 225,
end_angle: -45,
intervals: '50,100',
interval_color_indices: '1,2',
show_pointer: true,
number_format: 'SMART_NUMBER',
value_formatter: '{value}',
};
const chartResp = await apiPostChart(page, {
slice_name: sliceName,
viz_type: 'gauge_chart',
datasource_id: datasetId,
datasource_type: 'table',
params: JSON.stringify(chartParams),
});
expect(chartResp.ok()).toBe(true);
const chartBody = await chartResp.json();
// Normalize: API may return id at top level or inside result.
const chartId: number = chartBody.result?.id ?? chartBody.id;
if (!chartId) {
throw new Error(
`Chart creation returned no id. Response: ${JSON.stringify(chartBody)}`,
);
}
testAssets.trackChart(chartId);
const chartLayoutKey = `CHART-${chartId}`;
const positionJson = {
DASHBOARD_VERSION_KEY: 'v2',
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
GRID_ID: {
type: 'GRID',
id: 'GRID_ID',
children: ['ROW-1'],
parents: ['ROOT_ID'],
},
'ROW-1': {
type: 'ROW',
id: 'ROW-1',
children: [chartLayoutKey],
parents: ['ROOT_ID', 'GRID_ID'],
meta: { background: 'BACKGROUND_TRANSPARENT' },
},
[chartLayoutKey]: {
type: 'CHART',
id: chartLayoutKey,
children: [],
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
meta: {
chartId,
width: 6,
height: 60,
sliceName,
},
},
};
const dashResp = await apiPostDashboard(page, {
dashboard_title: `gauge_interval_colors_${Date.now()}`,
published: true,
position_json: JSON.stringify(positionJson),
json_metadata: JSON.stringify({ color_scheme: 'supersetColors' }),
});
expect(dashResp.ok()).toBe(true);
const dashBody = await dashResp.json();
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
testAssets.trackDashboard(dashboardId);
await apiPutChart(page, chartId, { dashboards: [dashboardId] });
const dashboardPage = new DashboardPage(page);
await dashboardPage.gotoById(dashboardId);
await dashboardPage.waitForLoad();
await dashboardPage.waitForChartsToLoad();
const canvas = page.locator('[data-test="chart-container"] canvas').first();
await canvas.waitFor({ state: 'visible', timeout: 30_000 });
// Read the configured interval colors back from the rendered canvas.
// Poll because the gauge paints shortly after the chart container appears.
const countColors = () =>
canvas.evaluate(
(el: HTMLCanvasElement, targets: Array<[number, number, number]>) => {
const ctx = el.getContext('2d');
if (!ctx) return targets.map(() => 0);
const { data } = ctx.getImageData(0, 0, el.width, el.height);
const counts = targets.map(() => 0);
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] < 200) continue;
for (let t = 0; t < targets.length; t += 1) {
const [r, g, b] = targets[t];
if (
Math.abs(data[i] - r) < 12 &&
Math.abs(data[i + 1] - g) < 12 &&
Math.abs(data[i + 2] - b) < 12
) {
counts[t] += 1;
}
}
}
return counts;
},
[COLOR_INTERVAL_1, COLOR_INTERVAL_2, COLOR_UNUSED_3],
);
// Capture the counts inside the poll so the assertions below run against the
// exact paint snapshot that satisfied the poll, not a second canvas read.
let counts: number[] = [0, 0, 0];
await expect
.poll(
async () => {
counts = await countColors();
return counts[0];
},
{ timeout: 20_000 },
)
.toBeGreaterThan(50);
const [interval1, interval2, unused3] = counts;
expect(
interval1,
'Gauge should paint the 1st interval color (#1FA8C9)',
).toBeGreaterThan(50);
expect(
interval2,
'Gauge should paint the 2nd interval color (#454E7C)',
).toBeGreaterThan(50);
expect(
unused3,
'Gauge must not paint the 3rd palette color (#5AC189) — indicates a shifted/fallback palette (#28766)',
).toBe(0);
},
);
@@ -1,309 +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.
*/
/**
* Regression for #33406: in a Pivot Table (v2) with nested rows, collapsing a
* row group with the [-] toggle should stay collapsed after the collapsed rows
* scroll out of the viewport and back. The bug reproduces specifically when the
* dashboard is embedded via an iframe the collapse/expand state lives in the
* pivot renderer's local React state (`collapsedRows` initialised to `{}`), so
* anything that remounts the chart resets it and the rows re-expand.
*
* This spec runs on the embedded harness (the only place the bug is reported to
* reproduce). It collapses a top-level row, scrolls the embedded dashboard so
* the pivot leaves and re-enters the viewport, and asserts the row is still
* collapsed.
*
* CI green => collapse state survives the scroll round-trip; merging closes
* #33406 and guards against regressions.
* CI red => the rows re-expanded; the bug is live and the fix belongs in
* plugin-chart-pivot-table (lift collapse state out of transient
* component state, e.g. persist `collapsedRows`/`collapsedCols`).
*
* NOTE: the embedded suite only runs when the embedded SDK bundle is built and
* INCLUDE_EMBEDDED=true (CI sets both). It is skipped otherwise.
*/
import { test, expect, Browser, BrowserContext, Page } from '@playwright/test';
import { createServer, IncomingMessage, ServerResponse, Server } from 'http';
import { AddressInfo, Socket } from 'net';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import {
apiEnableEmbedding,
getAccessToken,
getGuestToken,
} from '../../helpers/api/embedded';
import { apiPost, apiPut } from '../../helpers/api/requests';
import { apiPostDashboard, apiDeleteDashboard } from '../../helpers/api/dashboard';
import { apiDeleteChart } from '../../helpers/api/chart';
import { EmbeddedPage } from '../../pages/EmbeddedPage';
import { EMBEDDED } from '../../utils/constants';
const SUPERSET_DOMAIN = (() => {
const url = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8088';
return url.replace(/\/+$/, '');
})();
const SUPERSET_BASE_URL = SUPERSET_DOMAIN.endsWith('/')
? SUPERSET_DOMAIN
: `${SUPERSET_DOMAIN}/`;
const SDK_BUNDLE_PATH = join(
__dirname,
'../../../../superset-embedded-sdk/bundle/index.js',
);
const EMBED_APP_DIR = join(__dirname, '../../embedded-app');
const INDEX_HTML_PATH = join(EMBED_APP_DIR, 'index.html');
const DATASET_NAME = 'birth_names';
interface EmbedAppServer {
server: Server;
url: string;
close: () => Promise<void>;
}
async function startEmbedAppServer(): Promise<EmbedAppServer> {
const sockets = new Set<Socket>();
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
const urlPath = req.url?.split('?')[0] || '/';
if (urlPath === '/sdk/index.js') {
if (!existsSync(SDK_BUNDLE_PATH)) {
res.writeHead(404);
res.end(
'SDK bundle not found. Run: cd superset-embedded-sdk && npm ci && npm run build',
);
return;
}
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.end(readFileSync(SDK_BUNDLE_PATH));
return;
}
if (urlPath === '/' || urlPath === '/index.html') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(readFileSync(INDEX_HTML_PATH));
return;
}
res.writeHead(404);
res.end('Not found');
});
server.on('connection', socket => {
sockets.add(socket);
socket.once('close', () => sockets.delete(socket));
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.removeListener('error', reject);
resolve();
});
});
const address = server.address() as AddressInfo;
return {
server,
url: `http://127.0.0.1:${address.port}`,
close: () =>
new Promise<void>(resolve => {
for (const socket of sockets) socket.destroy();
sockets.clear();
server.close(() => resolve());
}),
};
}
function createAdminContext(browser: Browser): Promise<BrowserContext> {
return browser.newContext({
storageState: 'playwright/.auth/user.json',
baseURL: SUPERSET_BASE_URL,
});
}
async function findDatasetIdByName(page: Page, name: string): Promise<number> {
const query = `(filters:!((col:table_name,opr:eq,value:'${name}')))`;
const resp = await page.request.get(`api/v1/dataset/?q=${query}`);
const body = await resp.json();
if (!body.result?.length) {
throw new Error(`Dataset ${name} not found`);
}
return body.result[0].id;
}
test.describe('Embedded Pivot Table collapse state (#33406)', () => {
test.describe.configure({ mode: 'serial' });
test.setTimeout(90000);
let appServer: EmbedAppServer;
let accessToken: string;
let embedUuid: string;
let dashboardId: number;
let chartId: number;
test.beforeAll(async ({ browser }) => {
test.skip(
!existsSync(SDK_BUNDLE_PATH),
'Embedded SDK bundle not found. Build it with: cd superset-embedded-sdk && npm ci && npm run build',
);
appServer = await startEmbedAppServer();
const context = await createAdminContext(browser);
const setupPage = await context.newPage();
try {
const datasetId = await findDatasetIdByName(setupPage, DATASET_NAME);
const params = {
datasource: `${datasetId}__table`,
viz_type: 'pivot_table_v2',
groupbyRows: ['state', 'name'],
groupbyColumns: [],
metrics: ['count'],
metricsLayout: 'COLUMNS',
aggregateFunction: 'Count',
rowSubTotals: true,
rowTotals: true,
valueFormat: 'SMART_NUMBER',
row_limit: 1000,
order_desc: true,
};
const chartResp = await apiPost(setupPage, 'api/v1/chart/', {
slice_name: `pivot_collapse_repro_${Date.now()}`,
viz_type: 'pivot_table_v2',
datasource_id: datasetId,
datasource_type: 'table',
params: JSON.stringify(params),
});
chartId = (await chartResp.json()).id;
const chartLayoutKey = `CHART-${chartId}`;
const positionJson = {
DASHBOARD_VERSION_KEY: 'v2',
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
GRID_ID: {
type: 'GRID',
id: 'GRID_ID',
children: ['ROW-1'],
parents: ['ROOT_ID'],
},
'ROW-1': {
type: 'ROW',
id: 'ROW-1',
children: [chartLayoutKey],
parents: ['ROOT_ID', 'GRID_ID'],
meta: { background: 'BACKGROUND_TRANSPARENT' },
},
[chartLayoutKey]: {
type: 'CHART',
id: chartLayoutKey,
children: [],
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
meta: {
chartId,
width: 6,
height: 80,
sliceName: 'pivot_collapse_repro',
},
},
};
const dashResp = await apiPostDashboard(setupPage, {
dashboard_title: `pivot_collapse_repro_${Date.now()}`,
published: true,
position_json: JSON.stringify(positionJson),
});
const dashBody = await dashResp.json();
dashboardId = dashBody.id;
await apiPut(setupPage, `api/v1/chart/${chartId}`, {
dashboards: [dashboardId],
});
const embedded = await apiEnableEmbedding(setupPage, dashboardId);
embedUuid = embedded.uuid;
accessToken = await getAccessToken(setupPage);
} finally {
await context.close();
}
});
test.afterAll(async ({ browser }) => {
const context = await createAdminContext(browser);
try {
const cleanupPage = await context.newPage();
if (dashboardId !== undefined) {
await apiDeleteDashboard(cleanupPage, dashboardId, {
failOnStatusCode: false,
});
}
if (chartId !== undefined) {
await apiDeleteChart(cleanupPage, chartId, { failOnStatusCode: false });
}
} catch (err) {
// eslint-disable-next-line no-console
console.error('[pivot-collapse teardown] cleanup failed:', err);
} finally {
await context.close();
}
if (appServer) await appServer.close();
});
test('collapsed rows stay collapsed after a scroll round-trip', async ({
page,
}) => {
const embeddedPage = new EmbeddedPage(page);
await embeddedPage.exposeTokenFetcher(async () =>
getGuestToken(page, dashboardId, { accessToken }),
);
await embeddedPage.goto({
appUrl: appServer.url,
uuid: embedUuid,
supersetDomain: SUPERSET_DOMAIN,
});
await embeddedPage.waitForIframe();
await embeddedPage.waitForDashboardContent();
await embeddedPage.waitForChartRendered();
const rowLabels = embeddedPage.iframe.locator('.pvtRowLabel');
await expect
.poll(() => rowLabels.count(), { timeout: EMBEDDED.CHART_RENDER })
.toBeGreaterThan(1);
const expandedCount = await rowLabels.count();
// Collapse the first top-level row group via its [-] toggle. Scope to
// `.pvtTable` so we never match a stray `toggle` class elsewhere in the DOM.
await embeddedPage.iframe.locator('.pvtTable .toggle').first().click();
await expect
.poll(() => embeddedPage.iframe.locator('.pvtRowLabel').count(), {
timeout: EMBEDDED.CHART_RENDER,
})
.toBeLessThan(expandedCount);
const collapsedCount = await embeddedPage.iframe
.locator('.pvtRowLabel')
.count();
// Scroll the embedded dashboard so the pivot leaves the viewport, then back.
await embeddedPage.iframe.locator('body').evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
await page.waitForTimeout(800);
await embeddedPage.iframe.locator('body').evaluate(() => {
window.scrollTo(0, 0);
});
await page.waitForTimeout(1200);
// The collapsed group must remain collapsed (row-label count unchanged).
await expect(embeddedPage.iframe.locator('.pvtRowLabel')).toHaveCount(
collapsedCount,
);
});
});
@@ -33,7 +33,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.3.0"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"
@@ -34,6 +34,6 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0"
"react": "^18.2.0"
}
}
@@ -165,16 +165,6 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
return '';
};
const updatePopupPosition = () => {
const svgHeight = svg.node().getBoundingClientRect().height;
const [x, y] = d3.mouse(svg.node());
hoverPopup
.style('display', 'block')
.style('top', `${y + 30}px`)
.style('left', `${x}px`)
.classed('popup-at-bottom', y > (svgHeight * 2) / 3);
};
const mouseenter = function mouseenter(this: SVGPathElement, d: GeoFeature) {
// Darken color
let c: string = colorFn(d);
@@ -187,14 +177,21 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
region => region.country_id === d.properties.ISO,
);
hoverPopup.style('display', 'block').html(
`<div><strong>${getNameOfRegion(d)}</strong><br>${result.length > 0 ? formatter(result[0].metric) : ''}</div>`,
);
updatePopupPosition();
const position = d3.mouse(svg.node());
hoverPopup
.style('display', 'block')
.style('top', `${position[1] + 30}px`)
.style('left', `${position[0]}px`)
.html(
`<div><strong>${getNameOfRegion(d)}</strong><br>${result.length > 0 ? formatter(result[0].metric) : ''}</div>`,
);
};
const mousemove = function mousemove() {
updatePopupPosition();
const position = d3.mouse(svg.node());
hoverPopup
.style('top', `${position[1] + 30}px`)
.style('left', `${position[0]}px`);
};
const mouseout = function mouseout(this: SVGPathElement) {
@@ -86,10 +86,5 @@ export default styled(CountryMap)`
cursor: pointer;
stroke: ${theme.colorSplit};
}
.superset-legacy-chart-country-map .hover-popup.popup-at-bottom {
transform: translateY(-150%);
}
`}
`;
@@ -31,7 +31,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.3.0"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"
@@ -31,7 +31,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.3.0"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"
@@ -36,6 +36,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.3.0"
"react": "^18.2.0"
}
}
@@ -33,8 +33,8 @@
"@apache-superset/core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"
@@ -32,7 +32,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.3.0"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"
@@ -39,6 +39,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.3.0"
"react": "^18.2.0"
}
}
@@ -34,7 +34,7 @@
"fast-safe-stringify": "^2.1.1",
"lodash": "^4.18.1",
"nvd3-fork": "^2.0.5",
"dompurify": "^3.4.9",
"dompurify": "^3.4.7",
"prop-types": "^15.8.1",
"urijs": "^1.19.11"
},
@@ -43,6 +43,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"dayjs": "^1.11.21",
"react": "^18.3.0"
"react": "^18.2.0"
}
}
@@ -44,8 +44,8 @@
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"react": "^18.3.0",
"react-dom": "^18.3.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"
@@ -47,7 +47,7 @@
"geostyler-wfs-parser": "^3.0.1",
"ol": "^10.8.0",
"polished": "*",
"react": "^18.3.0",
"react-dom": "^18.3.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
@@ -38,7 +38,7 @@
"dayjs": "^1.11.21",
"echarts": "*",
"memoize-one": "*",
"react": "^18.3.0"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

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