Compare commits

..

2 Commits

Author SHA1 Message Date
Evan Rusackas
024aac2152 Merge branch 'master' into chore/sqlalchemy-min-version 2026-06-16 21:14:18 -07:00
Amin Ghadersohi
44f0667a55 chore(deps): raise SQLAlchemy lower bound to >=1.4.43
The python-oracledb (oracle+oracledb) dialect that the Oracle engine
spec now relies on was added in SQLAlchemy 1.4.43. The old `>=1.4`
floor allowed 1.4.0-1.4.42, where that dialect does not exist, so a
downstream resolver could install a SQLAlchemy that cannot connect to
Oracle. The lockfile already resolves to 1.4.54, so this is a no-op for
the pinned environment and only constrains library installs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 11:19:08 -07:00
168 changed files with 3222 additions and 6036 deletions

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

View File

@@ -47,7 +47,6 @@ jobs:
permissions:
actions: read
contents: read
pull-requests: read
security-events: write
strategy:

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:

View File

@@ -10,15 +10,25 @@ permissions:
contents: read
jobs:
build:
# Publishing uses npm trusted publishing (OIDC), so there is no NPM_TOKEN to
# gate on. Restrict to the canonical repo: forks cannot mint a valid OIDC
# token for this package and must not publish.
if: github.repository == 'apache/superset'
config:
runs-on: ubuntu-24.04
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
- name: "Check for secrets"
id: check
shell: bash
run: |
if [ -n "${NPM_TOKEN}" ]; then
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
fi
env:
NPM_TOKEN: ${{ (secrets.NPM_TOKEN != '') || '' }}
build:
needs: config
if: needs.config.outputs.has-secrets
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write # required for npm trusted publishing (OIDC)
defaults:
run:
working-directory: superset-embedded-sdk
@@ -26,23 +36,11 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# Note: registry-url is intentionally omitted. When set, actions/setup-node
# writes an .npmrc with `_authToken=${NODE_AUTH_TOKEN}` and a placeholder
# token, which makes npm attempt token auth and skip the OIDC
# trusted-publishing exchange. With no .npmrc auth line, npm authenticates
# via OIDC against the default registry (registry.npmjs.org).
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: "./superset-embedded-sdk/.nvmrc"
# Diagnostic: confirm GitHub OIDC is granted to this job. npm trusted
# publishing needs the id-token request env vars; "no" here means OIDC was
# not issued and publishing cannot authenticate.
- name: Check OIDC availability
run: |
if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL}" ]; then
echo "GitHub OIDC available to job: yes"
else
echo "GitHub OIDC available to job: no"
fi
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run ci:release
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -37,7 +37,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: "temurin"
java-version: "11"

View File

@@ -23,7 +23,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: "temurin"
java-version: "11"

View File

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

View File

@@ -1,11 +1,6 @@
# Python integration tests
name: Python-Integration
# Least-privilege default for GITHUB_TOKEN. Jobs that need more (e.g. OIDC for
# codecov uploads) opt in via their own job-level `permissions:` block.
permissions:
contents: read
on:
push:
branches:

View File

@@ -1,11 +1,6 @@
# Python unit tests
name: Python-Unit
# Least-privilege default for GITHUB_TOKEN. Jobs that need more (e.g. OIDC for
# codecov uploads) opt in via their own job-level `permissions:` block.
permissions:
contents: read
on:
push:
branches:

View File

@@ -396,10 +396,6 @@ categories:
url: https://www.techaudit.info
contributors: ["@ETselikov"]
- name: Tech Solution
url: https://www.tech-solution.com.ar/
contributors: ["@danteGiuliano", "@LeandroVallejos", "@McJaben", "@xJeree", "@zeo-return-null"]
- name: Tenable
url: https://www.tenable.com
contributors: ["@dflionis"]

View File

@@ -24,20 +24,6 @@ assists people when migrating to a new version.
## Next
### Pivot table First/Last aggregations follow data order
The pivot table chart's `First` and `Last` aggregations now return the first and last value in data (query result) order, instead of effectively returning the minimum and maximum. Existing pivot tables that use these aggregations for totals/subtotals may show different values after upgrading. For deterministic results, ensure the underlying query has a stable sort order.
### `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`.

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)..."

View File

@@ -22,24 +22,31 @@ level dependencies.
**Debian and Ubuntu**
The following command will ensure that the required dependencies are installed (tested on Ubuntu 20.04, 22.04, and 24.04):
Ubuntu **24.04** uses python 3.12 per default, which currently is not supported by Superset. You need to add a second python installation of 3.11 and install the required additional dependencies.
```bash
sudo apt-get install build-essential libssl-dev libffi-dev python3-dev python3-pip python3-venv libsasl2-dev libldap2-dev libpq-dev default-libmysqlclient-dev pkg-config
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.11 python3.11-dev python3.11-venv build-essential libssl-dev libffi-dev libsasl2-dev libldap2-dev default-libmysqlclient-dev
```
Refer to the
[pyproject.toml](https://github.com/apache/superset/blob/master/pyproject.toml) file for the list of
Python versions officially supported by Superset, and install a matching `python3` interpreter for
your distribution. The `libpq-dev` package is only needed if you intend to connect to (or use) a
PostgreSQL database; you can omit it otherwise.
In Ubuntu **20.04 and 22.04** the following command will ensure that the required dependencies are installed:
```bash
sudo apt-get install build-essential libssl-dev libffi-dev python3-dev python3-pip libsasl2-dev libldap2-dev default-libmysqlclient-dev
```
In Ubuntu **before 20.04** the following command will ensure that the required dependencies are installed:
```bash
sudo apt-get install build-essential libssl-dev libffi-dev python-dev python-pip libsasl2-dev libldap2-dev default-libmysqlclient-dev
```
**Fedora and RHEL-derivative Linux distributions**
Install the following packages using the `yum` package manager:
```bash
sudo yum install gcc gcc-c++ libffi-devel python3-devel python3-pip python3-wheel openssl-devel cyrus-sasl-devel openldap-devel
sudo yum install gcc gcc-c++ libffi-devel python-devel python-pip python-wheel openssl-devel cyrus-sasl-devel openldap-devel
```
In more recent versions of CentOS and Fedora, you may need to install a slightly different set of packages using `dnf`:

View File

@@ -70,9 +70,9 @@
"@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",
"baseline-browser-mapping": "^2.10.34",
"caniuse-lite": "^1.0.30001797",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
@@ -107,7 +107,7 @@
"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",
"webpack": "^5.107.2"

View File

@@ -4143,86 +4143,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"
@@ -5688,10 +5688,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.34, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.34"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz#dedb606362446777cfe328d30d4ee15056d06303"
integrity sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==
batch@0.6.1:
version "0.6.1"
@@ -7252,10 +7252,17 @@ domhandler@^5.0.2, domhandler@^5.0.3:
dependencies:
domelementtype "^2.3.0"
dompurify@^3.3.1, dompurify@^3.4.0:
version "3.4.11"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.11.tgz#29c8ba496475f279ef4015784068452fb14a0680"
integrity sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==
dompurify@^3.3.1:
version "3.4.2"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.2.tgz#f0ff81be682c485505097ba8195a058d8f575218"
integrity sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
dompurify@^3.4.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.1.tgz#521d04483ac12631b2aedf434a5f5390933b8789"
integrity sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
@@ -12263,10 +12270,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"
@@ -14999,9 +15006,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"

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.16.2 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.16.1 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

@@ -45,7 +45,7 @@ dependencies = [
"flask-cors>=6.0.0, <7.0",
"croniter>=6.2.2",
"cron-descriptor",
"cryptography>=48.0.0, <49.0.0",
"cryptography>=42.0.4, <47.0.0",
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <4.0.0",
"flask-appbuilder>=5.2.1, <6.0.0",
@@ -99,7 +99,7 @@ dependencies = [
"sshtunnel>=0.4.0, <0.5",
"simplejson>=4.1.1",
"slack_sdk>=3.19.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy>=1.4.43, <2", # 1.4.43 adds the python-oracledb (oracle+oracledb) dialect
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
"sqlglot>=30.8.0, <31",
# newer pandas needs 0.9+

View File

@@ -18,30 +18,5 @@
testpaths =
tests
python_files = *_test.py test_*.py *_tests.py *viz/utils.py
# `-p no:warnings` temporarily disabled in favor of more finely tuned `filterwarnings`.
#addopts = -p no:warnings
addopts = -p no:warnings
asyncio_mode = auto
# `ignore` is effectively equivalent to `-p no:warnings`.
# Always print RemovedIn20Warning when SQLALCHEMY_WARN_20=1.
# Additionally, raise errors for refactored RemovedIn20Warning cases to prevent regression.
filterwarnings =
ignore
always::sqlalchemy.exc.RemovedIn20Warning
# error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
# error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"SqlMetric" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"TableColumn" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:The ``as_declarative\(\)`` function is now available:sqlalchemy.exc.RemovedIn20Warning
# error:The autoload parameter is deprecated:sqlalchemy.exc.RemovedIn20Warning
# error:The connection.execute\(\) method:sqlalchemy.exc.RemovedIn20Warning
# error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
# error:The `database` package is deprecated:sqlalchemy.exc.RemovedIn20Warning
# error:The ``declarative_base\(\)`` function is now available:sqlalchemy.exc.RemovedIn20Warning
# error:The Engine.execute\(\) method is considered legacy:sqlalchemy.exc.RemovedIn20Warning
error:The legacy calling style of select\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
# error:The "whens" argument to case:sqlalchemy.exc.RemovedIn20Warning
# error:"User" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning

View File

@@ -26,7 +26,7 @@ filelock>=3.20.3,<4.0.0
brotli>=1.2.0,<2.0.0
numexpr>=2.9.0
# Security: CVE-2026-34073 (MEDIUM) - Improper Certificate Validation
cryptography>=48.0.0,<49.0.0
cryptography>=46.0.7,<47.0.0
# Security: Snyk - XSS vulnerability in Mako templates
mako>=1.3.11,<2.0.0
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers

View File

@@ -86,7 +86,7 @@ cron-descriptor==1.4.5
# via apache-superset (pyproject.toml)
croniter==6.2.2
# via apache-superset (pyproject.toml)
cryptography==48.0.1
cryptography==46.0.7
# via
# -r requirements/base.in
# apache-superset (pyproject.toml)
@@ -323,7 +323,7 @@ pyjwt==2.12.0
# redis
pynacl==1.6.2
# via paramiko
pyopenssl==26.2.0
pyopenssl==26.0.0
# via
# -r requirements/base.in
# shillelagh
@@ -344,6 +344,7 @@ python-dotenv==1.2.2
# via apache-superset (pyproject.toml)
pytz==2025.2
# via
# croniter
# flask-babel
# pandas
pyxlsb==1.0.10

View File

@@ -178,7 +178,7 @@ croniter==6.2.2
# via
# -c requirements/base-constraint.txt
# apache-superset
cryptography==48.0.1
cryptography==46.0.7
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -780,7 +780,7 @@ pynacl==1.6.2
# via
# -c requirements/base-constraint.txt
# paramiko
pyopenssl==26.2.0
pyopenssl==26.0.0
# via
# -c requirements/base-constraint.txt
# shillelagh
@@ -841,6 +841,7 @@ python-multipart==0.0.29
pytz==2025.2
# via
# -c requirements/base-constraint.txt
# croniter
# flask-babel
# pandas
# trino

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": [

View File

@@ -47,11 +47,7 @@ function logError(...args) {
execSync('npm publish --access public', { stdio: 'pipe' });
log(`published ${version} to npm`);
} catch (err) {
// npm writes failure details to stderr (auth/permission/registry
// errors in particular), so surface both streams to avoid masking
// the real cause in CI logs.
if (err.stdout) console.error(String(err.stdout));
if (err.stderr) console.error(String(err.stderr));
console.error(String(err.stdout));
logError('Encountered an error, details should be above');
process.exitCode = 1;
}

View File

@@ -107,13 +107,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',
},
],
],

File diff suppressed because it is too large Load Diff

View File

@@ -173,7 +173,7 @@
"d3-color": "^3.1.0",
"d3-scale": "^4.0.2",
"dayjs": "^1.11.21",
"dom-to-image-more": "^3.9.0",
"dom-to-image-more": "^3.7.2",
"dom-to-pdf": "^0.3.2",
"echarts": "^5.6.0",
"fast-glob": "^3.3.2",
@@ -194,7 +194,7 @@
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.18.1",
"mapbox-gl": "^3.24.0",
"markdown-to-jsx": "^9.8.2",
"markdown-to-jsx": "^9.8.1",
"match-sorter": "^8.3.0",
"memoize-one": "^5.2.1",
"mousetrap": "^1.6.5",
@@ -203,13 +203,13 @@
"ol": "^10.9.0",
"query-string": "9.4.0",
"re-resizable": "^6.11.2",
"react": "^18.3.0",
"react": "^18.2.0",
"react-arborist": "^3.10.1",
"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",
@@ -265,12 +265,12 @@
"@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.2",
"@storybook/addon-links": "10.4.2",
"@storybook/react-webpack5": "10.4.2",
"@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",
@@ -284,9 +284,9 @@
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.9.3",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/node": "^25.9.2",
"@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",
@@ -304,7 +304,7 @@
"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.35",
"baseline-browser-mapping": "^2.10.34",
"cheerio": "1.2.0",
"concurrently": "^10.0.3",
"copy-webpack-plugin": "^14.0.0",
@@ -325,7 +325,7 @@
"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.3",
"eslint-plugin-storybook": "10.4.2",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -346,17 +346,16 @@
"open-cli": "^9.0.0",
"oxlint": "^1.69.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.2",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.1",
@@ -370,7 +369,7 @@
"webpack": "^5.107.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^7.0.3",
"webpack-dev-server": "^5.2.5",
"webpack-dev-server": "^5.2.4",
"webpack-manifest-plugin": "^6.0.1",
"webpack-sources": "^3.5.0",
"webpack-visualizer-plugin2": "^2.0.0"

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,7 +43,7 @@
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.21",
"dompurify": "^3.4.11",
"dompurify": "^3.4.9",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
"jed": "^1.1.1",
@@ -77,7 +77,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.1",
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.3",
"@types/node": "^25.9.2",
"@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": "*"
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,13 +52,6 @@ const SupersetClient: SupersetClientInterface = {
request: request => getInstance().request(request),
getCSRFToken: () => getInstance().getCSRFToken(),
getUrl: (...args) => getInstance().getUrl(...args),
get guestTokenHeaderName() {
try {
return getInstance().guestTokenHeaderName;
} catch {
return 'X-GuestToken';
}
},
};
export default SupersetClient;

View File

@@ -163,7 +163,6 @@ export interface SupersetClientInterface extends Pick<
configure: (config?: ClientConfig) => SupersetClientInterface;
reset: () => void;
getCSRFToken: () => CsrfPromise;
guestTokenHeaderName?: string;
}
export type SupersetClientResponse = Response | JsonResponse | TextResponse;

View File

@@ -172,15 +172,4 @@ describe('SupersetClient', () => {
const token = await SupersetClient.getCSRFToken();
expect(token).toBe('my_token');
});
test('guestTokenHeaderName returns the configured header name when instance exists', () => {
SupersetClient.configure({ guestTokenHeaderName: 'X-Custom-Guest' });
expect(SupersetClient.guestTokenHeaderName).toBe('X-Custom-Guest');
});
test('guestTokenHeaderName returns default X-GuestToken when instance is not configured', () => {
// Ensure instance is reset (afterEach calls SupersetClient.reset())
// Access the property without calling configure() first
expect(SupersetClient.guestTokenHeaderName).toBe('X-GuestToken');
});
});

View File

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

View File

@@ -34,6 +34,6 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0"
"react": "^18.2.0"
}
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -36,6 +36,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.3.0"
"react": "^18.2.0"
}
}

View File

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

View File

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

View File

@@ -39,6 +39,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.3.0"
"react": "^18.2.0"
}
}

View File

@@ -34,7 +34,7 @@
"fast-safe-stringify": "^2.1.1",
"lodash": "^4.18.1",
"nvd3-fork": "^2.0.5",
"dompurify": "^3.4.11",
"dompurify": "^3.4.9",
"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"
}
}

View File

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

View File

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

View File

@@ -38,7 +38,7 @@
"dayjs": "^1.11.21",
"echarts": "*",
"memoize-one": "*",
"react": "^18.3.0"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -79,11 +79,10 @@ export default function EchartsMixedTimeseries({
values.length === 0
? []
: currentGroupBy.map((col, idx) => {
const val: DataRecordValue[] = groupbyValues.map(v => {
const metricsCount = v.length - currentGroupBy.length;
return v[metricsCount + idx];
});
if (val.every(vv => vv == null))
const val: DataRecordValue[] = groupbyValues.map(
v => v[idx],
);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL' as const,

View File

@@ -129,11 +129,8 @@ export default function EchartsTimeseries({
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => {
const metricsCount = v.length - groupby.length;
return v[metricsCount + idx];
});
if (val.every(vv => vv == null))
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL' as const,

View File

@@ -65,11 +65,8 @@ const getCrossFilterDataMask =
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => {
const metricsCount = v.length - groupby.length;
return v[metricsCount + idx];
});
if (val.every(vv => vv == null))
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL' as const,

View File

@@ -1,182 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { QueryFormData } from '@superset-ui/core';
import {
BaseTransformedProps,
CrossFilterTransformedProps,
} from '../../src/types';
import { allEventHandlers } from '../../src/utils/eventHandlers';
function buildProps(
overrides: Partial<
BaseTransformedProps<QueryFormData> & CrossFilterTransformedProps
>,
): BaseTransformedProps<QueryFormData> & CrossFilterTransformedProps {
return {
formData: {} as QueryFormData,
height: 400,
width: 800,
queriesData: [],
filterState: {},
onContextMenu: jest.fn(),
setDataMask: jest.fn(),
emitCrossFilters: true,
groupby: [],
labelMap: {},
selectedValues: {},
coltypeMapping: {},
...overrides,
} as BaseTransformedProps<QueryFormData> & CrossFilterTransformedProps;
}
test('cross-filter emits dimension value, not metric label, for single-metric chart', () => {
const setDataMask = jest.fn();
const props = buildProps({
groupby: ['topics'],
labelMap: {
cancellations: ['cancellations'],
},
selectedValues: {},
setDataMask,
});
const handlers = allEventHandlers(props);
handlers.click({ name: 'cancellations' });
expect(setDataMask).toHaveBeenCalledWith(
expect.objectContaining({
extraFormData: {
filters: [
{
col: 'topics',
op: 'IN',
val: ['cancellations'],
},
],
},
}),
);
});
test('cross-filter emits dimension value, not metric label, for multi-metric stacked chart', () => {
const setDataMask = jest.fn();
// For multi-metric stacked bars, labelMap values include
// [metricLabel, ...dimensionValues]
const props = buildProps({
groupby: ['topics'],
labelMap: {
'Intent, cancellations': ['Intent', 'cancellations'],
'Intent, renewals': ['Intent', 'renewals'],
'Volume, cancellations': ['Volume', 'cancellations'],
'Volume, renewals': ['Volume', 'renewals'],
},
selectedValues: {},
setDataMask,
});
const handlers = allEventHandlers(props);
handlers.click({ name: 'Intent, cancellations' });
expect(setDataMask).toHaveBeenCalledWith(
expect.objectContaining({
extraFormData: {
filters: [
{
col: 'topics',
op: 'IN',
val: ['cancellations'],
},
],
},
}),
);
});
test('cross-filter emits correct values for multi-metric chart with multiple groupby columns', () => {
const setDataMask = jest.fn();
const props = buildProps({
groupby: ['region', 'topics'],
labelMap: {
'Intent, US, cancellations': ['Intent', 'US', 'cancellations'],
},
selectedValues: {},
setDataMask,
});
const handlers = allEventHandlers(props);
handlers.click({ name: 'Intent, US, cancellations' });
expect(setDataMask).toHaveBeenCalledWith(
expect.objectContaining({
extraFormData: {
filters: [
{
col: 'region',
op: 'IN',
val: ['US'],
},
{
col: 'topics',
op: 'IN',
val: ['cancellations'],
},
],
},
}),
);
});
test('cross-filter deselects previously selected value', () => {
const setDataMask = jest.fn();
const props = buildProps({
groupby: ['topics'],
labelMap: {
cancellations: ['cancellations'],
},
selectedValues: { 0: 'cancellations' },
setDataMask,
});
const handlers = allEventHandlers(props);
handlers.click({ name: 'cancellations' });
expect(setDataMask).toHaveBeenCalledWith(
expect.objectContaining({
extraFormData: {
filters: [],
},
}),
);
});
test('cross-filter does nothing when emitCrossFilters is false', () => {
const setDataMask = jest.fn();
const props = buildProps({
groupby: ['topics'],
labelMap: { cancellations: ['cancellations'] },
selectedValues: {},
setDataMask,
emitCrossFilters: false,
});
const handlers = allEventHandlers(props);
handlers.click({ name: 'cancellations' });
expect(setDataMask).not.toHaveBeenCalled();
});

View File

@@ -39,9 +39,9 @@
"handlebars": "^4.7.8",
"lodash": "^4.18.1",
"dayjs": "^1.11.21",
"react": "^18.3.0",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-dom": "^18.3.0"
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/jest": "^30.0.0",

View File

@@ -23,7 +23,7 @@ import {
} from '@superset-ui/chart-controls';
import { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import { useTheme, useThemeMode } from '@apache-superset/core/theme';
import { useTheme } from '@apache-superset/core/theme';
import { InfoTooltip } from '@superset-ui/core/components';
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
@@ -37,7 +37,6 @@ const HandlebarsTemplateControl = (
props: CustomControlConfig<HandlebarsCustomControlProps>,
) => {
const theme = useTheme();
const isDarkMode = useThemeMode();
const val = String(
props?.value ? props?.value : props?.default ? props?.default : '',
);
@@ -66,7 +65,7 @@ const HandlebarsTemplateControl = (
</div>
</ControlHeader>
<CodeEditor
theme={isDarkMode ? 'dark' : 'light'}
theme="dark"
value={val}
onChange={source => {
debounceFunc(props.onChange, source || '');

View File

@@ -22,7 +22,7 @@ import {
sharedControls,
} from '@superset-ui/chart-controls';
import { t } from '@apache-superset/core/translation';
import { useTheme, useThemeMode } from '@apache-superset/core/theme';
import { useTheme } from '@apache-superset/core/theme';
import { InfoTooltip } from '@superset-ui/core/components';
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
@@ -35,7 +35,6 @@ interface StyleCustomControlProps {
const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
const theme = useTheme();
const isDarkMode = useThemeMode();
const htmlSanitization = props.htmlSanitization ?? true;
const defaultValue = props?.value
@@ -64,7 +63,7 @@ const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
</div>
</ControlHeader>
<CodeEditor
theme={isDarkMode ? 'dark' : 'light'}
theme="dark"
mode="css"
value={props.value}
defaultValue={defaultValue}

View File

@@ -1,77 +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 { ComponentType } from 'react';
import { CustomControlItem } from '@superset-ui/chart-controls';
import { render } from 'spec/helpers/testing-library';
import { useThemeMode } from '@apache-superset/core/theme';
import { handlebarsTemplateControlSetItem } from '../../src/plugin/controls/handlebarTemplate';
import { styleControlSetItem } from '../../src/plugin/controls/style';
const mockCodeEditor = jest.fn((_props: { theme?: string }) => null);
jest.mock('../../src/components/CodeEditor/CodeEditor', () => ({
CodeEditor: (props: { theme?: string }) => mockCodeEditor(props),
}));
jest.mock('@apache-superset/core/theme', () => ({
...jest.requireActual('@apache-superset/core/theme'),
useThemeMode: jest.fn(),
}));
const HandlebarsTemplateControl = (
handlebarsTemplateControlSetItem as CustomControlItem
).config.type as ComponentType<{ value: string; onChange: () => void }>;
const StyleControl = (styleControlSetItem as CustomControlItem).config
.type as ComponentType<{ value: string; onChange: () => void }>;
const mockedUseThemeMode = useThemeMode as jest.Mock;
afterEach(() => jest.clearAllMocks());
test('Handlebars Template editor uses the light Ace theme in a light UI', () => {
mockedUseThemeMode.mockReturnValue(false);
render(<HandlebarsTemplateControl value="x" onChange={jest.fn()} />);
expect(mockCodeEditor).toHaveBeenCalledWith(
expect.objectContaining({ theme: 'light' }),
);
});
test('Handlebars Template editor uses the dark Ace theme in a dark UI', () => {
mockedUseThemeMode.mockReturnValue(true);
render(<HandlebarsTemplateControl value="x" onChange={jest.fn()} />);
expect(mockCodeEditor).toHaveBeenCalledWith(
expect.objectContaining({ theme: 'dark' }),
);
});
test('CSS Styles editor uses the light Ace theme in a light UI', () => {
mockedUseThemeMode.mockReturnValue(false);
render(<StyleControl value="x" onChange={jest.fn()} />);
expect(mockCodeEditor).toHaveBeenCalledWith(
expect.objectContaining({ theme: 'light' }),
);
});
test('CSS Styles editor uses the dark Ace theme in a dark UI', () => {
mockedUseThemeMode.mockReturnValue(true);
render(<StyleControl value="x" onChange={jest.fn()} />);
expect(mockCodeEditor).toHaveBeenCalledWith(
expect.objectContaining({ theme: 'dark' }),
);
});

View File

@@ -33,8 +33,8 @@
"@superset-ui/core": "*",
"lodash": "^4.18.1",
"prop-types": "*",
"react": "^18.3.0",
"react-dom": "^18.3.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/types": "^7.29.7",

View File

@@ -473,10 +473,14 @@ const baseAggregatorTemplates = {
extremes(mode: string, formatter = usFmt) {
return function ([attr]: string[]) {
return function () {
return function (data: any) {
return {
val: null as any,
currencySet: new Set<string>(),
sorter: getSort(
typeof data !== 'undefined' ? data.sorters : null,
attr,
),
push(record: PivotRecord) {
const x = record[attr];
if (['min', 'max'].includes(mode)) {
@@ -495,9 +499,21 @@ const baseAggregatorTemplates = {
this.val !== null ? this.val : coercedValue,
);
}
} else if (mode === 'first') {
this.val = this.val === null ? x : this.val;
} else if (mode === 'last') {
} else if (
mode === 'first' &&
this.sorter(
x as any,
this.val !== null ? this.val : (x as any),
) <= 0
) {
this.val = x;
} else if (
mode === 'last' &&
this.sorter(
x as any,
this.val !== null ? this.val : (x as any),
) >= 0
) {
this.val = x;
}
if (

View File

@@ -1,61 +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 { aggregators } from '../../src/react-pivottable/utilities';
import type { PivotRecord } from '../../src/react-pivottable/utilities';
// Records may legitimately carry null values for an attribute; PivotRecord only
// models the non-null cell types, so loosen the type at the test boundary.
type TestRecord = Record<string, string | number | boolean | null>;
type ExtremesAggregator = 'First' | 'Last' | 'Minimum' | 'Maximum';
// Build an `extremes`-based aggregator (First/Last/Minimum/Maximum) for `attr`
// and feed it the records in order, returning the resulting value.
const aggregate = (name: ExtremesAggregator, records: TestRecord[]) => {
const aggregator = aggregators[name](['x'])();
records.forEach(record => aggregator.push(record as PivotRecord));
return aggregator.value();
};
test('First returns the first value in data order, not the minimum', () => {
// Descending input: the buggy implementation returned the minimum (1).
expect(aggregate('First', [{ x: 5 }, { x: 3 }, { x: 1 }])).toBe(5);
expect(aggregate('First', [{ x: 1 }, { x: 3 }, { x: 5 }])).toBe(1);
});
test('Last returns the last value in data order, not the maximum', () => {
// Ascending input: the buggy implementation returned the maximum (5).
expect(aggregate('Last', [{ x: 1 }, { x: 3 }, { x: 5 }])).toBe(5);
expect(aggregate('Last', [{ x: 5 }, { x: 3 }, { x: 1 }])).toBe(1);
});
test('First keeps the first non-null value, skipping a leading null', () => {
expect(aggregate('First', [{ x: null }, { x: 7 }, { x: 9 }])).toBe(7);
});
test('First preserves a falsy first value such as zero', () => {
expect(aggregate('First', [{ x: 0 }, { x: 5 }])).toBe(0);
});
test('Minimum and Maximum still compute extremes regardless of order', () => {
const records = [{ x: 3 }, { x: 1 }, { x: 5 }, { x: 2 }];
expect(aggregate('Minimum', records)).toBe(1);
expect(aggregate('Maximum', records)).toBe(5);
});

View File

@@ -36,8 +36,8 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.3.0",
"react-dom": "^18.3.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -45,8 +45,8 @@
"@testing-library/user-event": "*",
"@types/react": "*",
"match-sorter": "^8.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -39,7 +39,7 @@
"@superset-ui/core": "*",
"@types/lodash": "*",
"@types/react": "*",
"react": "^18.3.0"
"react": "^18.2.0"
},
"devDependencies": {
"@types/d3-cloud": "^1.2.9"

View File

@@ -67,8 +67,8 @@
"@superset-ui/core": "*",
"dayjs": "^1.11.21",
"mapbox-gl": ">=1.0.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"peerDependenciesMeta": {
"mapbox-gl": {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { AriaAttributes, Ref } from 'react';
import { AriaAttributes } from 'react';
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import jQuery from 'jquery';
@@ -98,39 +98,31 @@ jest.mock('rehype-raw', () => () => jest.fn());
// Tests should override this when needed
jest.mock('@superset-ui/core/components/Icons/AsyncIcon', () => ({
__esModule: true,
// eslint-disable-next-line global-require
default: require('react').forwardRef(
(
{
fileName,
role,
'aria-label': ariaLabel,
onClick,
...rest
}: {
fileName: string;
role?: string;
'aria-label'?: AriaAttributes['aria-label'];
onClick?: () => void;
},
ref: Ref<HTMLSpanElement>,
) => {
// Simple mock that provides the essential attributes for testing
const label =
ariaLabel || fileName?.replace(/_/g, '-').toLowerCase() || '';
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<span
ref={ref}
role={role || (onClick ? 'button' : 'img')}
aria-label={label}
data-test={label}
onClick={onClick}
{...rest}
/>
);
},
),
default: ({
fileName,
role,
'aria-label': ariaLabel,
onClick,
...rest
}: {
fileName: string;
role?: string;
'aria-label'?: AriaAttributes['aria-label'];
onClick?: () => void;
}) => {
// Simple mock that provides the essential attributes for testing
const label = ariaLabel || fileName?.replace(/_/g, '-').toLowerCase() || '';
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<span
role={role || (onClick ? 'button' : 'img')}
aria-label={label}
data-test={label}
onClick={onClick}
{...rest}
/>
);
},
StyledIcon: ({
component: Component,
role,

View File

@@ -1420,6 +1420,39 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('syncTable', () => {
test('updates the table schema state in the backend', () => {
expect.assertions(4);
const tableName = 'table';
const schemaName = 'schema';
const store = mockStore(initialState);
const expectedActionTypes = [
actions.MERGE_TABLE, // syncTable
];
const request = actions.syncTable(
query as any,
tableName as any,
schemaName,
);
return request(store.dispatch, store.getState, undefined).then(() => {
expect(store.getActions().map(a => a.type)).toEqual(
expectedActionTypes,
);
expect(store.getActions()[0].prepend).toBeFalsy();
expect(
fetchMock.callHistory.calls(updateTableSchemaEndpoint),
).toHaveLength(1);
// tab state is not updated, since no query was run
expect(
fetchMock.callHistory.calls(updateTabStateEndpoint),
).toHaveLength(0);
});
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('runTablePreviewQuery', () => {
const results = {

View File

@@ -1346,6 +1346,52 @@ export function runTablePreviewQuery(
};
}
export interface TableMetaData {
columns?: unknown[];
selectStar?: string;
primaryKey?: unknown;
foreignKeys?: unknown[];
indexes?: unknown[];
}
export function syncTable(
table: Table,
tableMetadata: TableMetaData,
finalQueryEditorId?: string,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
const finalTable = { ...table, queryEditorId: finalQueryEditorId };
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
? SupersetClient.post({
endpoint: encodeURI('/tableschemaview/'),
postPayload: { table: { ...tableMetadata, ...finalTable } },
})
: Promise.resolve({ json: { id: table.id } });
return sync
.then(({ json: resultJson }) => {
const newTable = { ...table, id: `${resultJson.id}` };
dispatch(
mergeTable({
...newTable,
expanded: true,
initialized: true,
}),
);
})
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while fetching table metadata. ' +
'Please contact your administrator.',
),
),
),
);
};
}
export function changeDataPreviewId(
oldQueryId: string,
newQuery: Query,

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { act } from 'react';
import { act } from 'react-dom/test-utils';
import { QueryState } from '@superset-ui/core';
import fetchMock from 'fetch-mock';
import configureStore from 'redux-mock-store';

View File

@@ -189,19 +189,15 @@ const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
placement="bottomLeft"
trigger="click"
>
{/* Wrap in a span so the Popover can attach a ref without relying
on findDOMNode (deprecated in React 18+). */}
<span>
<DatabaseSelector
key={`db-selector-${db ? db.id : 'no-db'}:${catalog ?? 'no-catalog'}:${
schema ?? 'no-schema'
}`}
{...dbSelectorProps}
emptyState={<EmptyState />}
sqlLabMode
onOpenModal={openSelectorModal}
/>
</span>
<DatabaseSelector
key={`db-selector-${db ? db.id : 'no-db'}:${catalog ?? 'no-catalog'}:${
schema ?? 'no-schema'
}`}
{...dbSelectorProps}
emptyState={<EmptyState />}
sqlLabMode
onOpenModal={openSelectorModal}
/>
</Popover>
<StyledDivider />
<TableExploreTree queryEditorId={activeQEId} />

View File

@@ -0,0 +1,464 @@
/**
* 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 { isValidElement } from 'react';
import fetchMock from 'fetch-mock';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import TableElement, { Column } from 'src/SqlLab/components/TableElement';
import { table, initialState } from 'src/SqlLab/fixtures';
import { render, waitFor, fireEvent } from 'spec/helpers/testing-library';
import * as sqlLabActions from 'src/SqlLab/actions/sqlLab';
import { QueryEditor } from 'src/SqlLab/types';
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
isFeatureEnabled: jest.fn(),
}));
const mockedIsFeatureEnabled = isFeatureEnabled as jest.Mock;
jest.mock('@superset-ui/core/components/Loading', () => ({
Loading: () => <div data-test="mock-loading" />,
}));
jest.mock('@superset-ui/core/components/IconTooltip', () => ({
IconTooltip: ({
onClick,
tooltip,
}: {
onClick: () => void;
tooltip: string;
}) => (
<button type="button" data-test="mock-icon-tooltip" onClick={onClick}>
{tooltip}
</button>
),
}));
jest.mock(
'src/SqlLab/components/ColumnElement',
() =>
({ column }: { column: Column }) => (
<div data-test="mock-column-element">{column.name}</div>
),
);
const getTableMetadataEndpoint =
/\/api\/v1\/database\/\d+\/table_metadata\/(?:\?.*)?$/;
const getExtraTableMetadataEndpoint =
/\/api\/v1\/database\/\d+\/table_metadata\/extra\/(?:\?.*)?$/;
const updateTableSchemaExpandedEndpoint = 'glob:*/tableschemaview/*/expanded';
const updateTableSchemaEndpoint = 'glob:*/tableschemaview/';
beforeEach(() => {
fetchMock.get(getTableMetadataEndpoint, table);
fetchMock.get(getExtraTableMetadataEndpoint, {});
fetchMock.post(updateTableSchemaExpandedEndpoint, {});
fetchMock.post(updateTableSchemaEndpoint, {});
});
afterEach(() => fetchMock.clearHistory().removeRoutes());
const mockedProps = {
table: {
...table,
initialized: true,
},
activeKey: [table.id],
};
const createStateWithQueryEditor = (queryEditor: Partial<QueryEditor>) => ({
...initialState,
sqlLab: {
...initialState.sqlLab,
queryEditors: [queryEditor],
},
});
const setupSyncTableTest = () => {
const spy = jest.spyOn(sqlLabActions, 'syncTable');
mockedIsFeatureEnabled.mockImplementation(
featureFlag => featureFlag === FeatureFlag.SqllabBackendPersistence,
);
fetchMock.removeRoute(updateTableSchemaEndpoint);
fetchMock.post(
updateTableSchemaEndpoint,
{ id: 100 },
{ name: updateTableSchemaEndpoint },
);
return spy;
};
test('renders', () => {
expect(isValidElement(<TableElement table={table} />)).toBe(true);
});
test('renders with props', () => {
expect(isValidElement(<TableElement {...mockedProps} />)).toBe(true);
});
test('has 4 IconTooltip elements', async () => {
const { getAllByTestId } = render(<TableElement {...mockedProps} />, {
useRedux: true,
initialState,
});
await waitFor(() =>
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(6),
);
});
test('has 14 columns', async () => {
const { getAllByTestId } = render(<TableElement {...mockedProps} />, {
useRedux: true,
initialState,
});
await waitFor(() =>
expect(getAllByTestId('mock-column-element')).toHaveLength(14),
);
});
test('fades table', async () => {
const { getAllByTestId } = render(<TableElement {...mockedProps} />, {
useRedux: true,
initialState,
});
await waitFor(() =>
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(6),
);
const style = window.getComputedStyle(getAllByTestId('fade')[0]);
expect(style.opacity).toBe('0');
fireEvent.mouseEnter(getAllByTestId('table-element-header-container')[0]);
await waitFor(() =>
expect(window.getComputedStyle(getAllByTestId('fade')[0]).opacity).toBe(
'1',
),
);
});
test('sorts columns', async () => {
const { getAllByTestId, getByText } = render(
<TableElement {...mockedProps} />,
{
useRedux: true,
initialState,
},
);
await waitFor(() =>
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(6),
);
expect(
getAllByTestId('mock-column-element').map(el => el.textContent),
).toEqual(table.columns.map(col => col.name));
fireEvent.click(getByText('Sort columns alphabetically'));
const sorted = table.columns.map(col => col.name).sort();
expect(
getAllByTestId('mock-column-element').map(el => el.textContent),
).toEqual(sorted);
expect(getAllByTestId('mock-column-element')[0]).toHaveTextContent('active');
});
test('removes the table', async () => {
const updateTableSchemaEndpoint = 'glob:*/tableschemaview/*';
fetchMock.delete(updateTableSchemaEndpoint, {});
mockedIsFeatureEnabled.mockImplementation(
featureFlag => featureFlag === FeatureFlag.SqllabBackendPersistence,
);
const { getAllByTestId, getByText } = render(
<TableElement {...mockedProps} />,
{
useRedux: true,
initialState,
},
);
await waitFor(() =>
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(6),
);
expect(fetchMock.callHistory.calls(updateTableSchemaEndpoint)).toHaveLength(
0,
);
fireEvent.click(getByText('Remove table preview'));
await waitFor(() =>
expect(fetchMock.callHistory.calls(updateTableSchemaEndpoint)).toHaveLength(
1,
),
);
mockedIsFeatureEnabled.mockClear();
});
test('fetches table metadata when expanded', async () => {
render(<TableElement {...mockedProps} />, {
useRedux: true,
initialState,
});
expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength(0);
expect(
fetchMock.callHistory.calls(getExtraTableMetadataEndpoint),
).toHaveLength(0);
await waitFor(() =>
expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength(
1,
),
);
expect(
fetchMock.callHistory.calls(updateTableSchemaExpandedEndpoint),
).toHaveLength(0);
expect(
fetchMock.callHistory.calls(getExtraTableMetadataEndpoint),
).toHaveLength(1);
});
test('refreshes table metadata when triggered', async () => {
const { getAllByTestId, getByText } = render(
<TableElement {...mockedProps} />,
{
useRedux: true,
initialState,
},
);
await waitFor(() =>
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(6),
);
expect(fetchMock.callHistory.calls(updateTableSchemaEndpoint)).toHaveLength(
0,
);
expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength(1);
fireEvent.click(getByText('Refresh table schema'));
await waitFor(() =>
expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength(
2,
),
);
await waitFor(() =>
expect(fetchMock.callHistory.calls(updateTableSchemaEndpoint)).toHaveLength(
1,
),
);
});
test('calls syncTable with valid backend ID when query editor has tabViewId', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: false,
queryEditorId: 'temp-id-123',
};
const state = createStateWithQueryEditor({
id: 'temp-id-123',
tabViewId: '42',
inLocalStorage: false,
name: 'Test Editor',
});
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
useRedux: true,
initialState: state,
});
await waitFor(() => {
expect(syncTableSpy).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Object),
'42', // finalQueryEditorId
);
});
syncTableSpy.mockRestore();
});
test('does not call syncTable when query editor is in localStorage', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: false,
queryEditorId: 'local-id',
};
const state = createStateWithQueryEditor({
id: 'local-id',
tabViewId: undefined,
inLocalStorage: true,
name: 'Local Editor',
});
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
useRedux: true,
initialState: state,
});
await waitFor(() => {
expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength(
1,
);
});
await new Promise(resolve => setTimeout(resolve, 100));
expect(syncTableSpy).not.toHaveBeenCalled();
syncTableSpy.mockRestore();
});
test('does not call syncTable with non-numeric queryEditorId', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: false,
queryEditorId: 'not-a-number',
};
const state = createStateWithQueryEditor({
id: 'not-a-number',
tabViewId: 'also-not-a-number',
inLocalStorage: false,
name: 'Invalid Editor',
});
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
useRedux: true,
initialState: state,
});
await waitFor(() => {
expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength(
1,
);
});
await new Promise(resolve => setTimeout(resolve, 100));
expect(syncTableSpy).not.toHaveBeenCalled();
syncTableSpy.mockRestore();
});
test('does not call syncTable for already initialized tables', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: true, // Already initialized
queryEditorId: '789',
};
const state = createStateWithQueryEditor({
id: '789',
tabViewId: '789',
inLocalStorage: false,
name: 'Initialized Editor',
});
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
useRedux: true,
initialState: state,
});
await waitFor(() => {
expect(fetchMock.callHistory.calls(getTableMetadataEndpoint)).toHaveLength(
1,
);
});
await new Promise(resolve => setTimeout(resolve, 100));
expect(syncTableSpy).not.toHaveBeenCalled();
syncTableSpy.mockRestore();
});
test('calls syncTable after query editor is migrated from localStorage', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: false,
queryEditorId: 'temp-editor-id',
};
// Start with editor in localStorage
const localState = createStateWithQueryEditor({
id: 'temp-editor-id',
tabViewId: undefined,
inLocalStorage: true,
name: 'Temp Editor',
});
const { rerender } = render(
<TableElement table={testTable} activeKey={[testTable.id]} />,
{
useRedux: true,
initialState: localState,
},
);
await new Promise(resolve => setTimeout(resolve, 100));
expect(syncTableSpy).not.toHaveBeenCalled();
const migratedState = createStateWithQueryEditor({
id: 'temp-editor-id',
tabViewId: '999',
inLocalStorage: false,
name: 'Temp Editor',
});
rerender(<TableElement table={testTable} activeKey={[testTable.id]} />);
const { unmount } = render(
<TableElement table={testTable} activeKey={[testTable.id]} />,
{
useRedux: true,
initialState: migratedState,
},
);
await waitFor(() => {
expect(syncTableSpy).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Object),
'999',
);
});
unmount();
syncTableSpy.mockRestore();
});
test('passes numeric queryEditorId validation', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: false,
queryEditorId: 'editor-123',
};
const state = createStateWithQueryEditor({
id: 'editor-123',
tabViewId: '456',
inLocalStorage: false,
name: 'Valid Editor',
});
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
useRedux: true,
initialState: state,
});
await waitFor(() => {
expect(syncTableSpy).toHaveBeenCalled();
const [, , finalQueryEditorId] = syncTableSpy.mock.calls[0];
// Verify it's a valid numeric string
expect(Number.isNaN(Number(finalQueryEditorId))).toBe(false);
expect(typeof finalQueryEditorId).toBe('string');
expect(finalQueryEditorId).toMatch(/^\d+$/);
});
syncTableSpy.mockRestore();
});

View File

@@ -0,0 +1,420 @@
/**
* 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 { useState, useRef, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/SqlLab/hooks/useAppDispatch';
import type { QueryEditor, SqlLabRootState, Table } from 'src/SqlLab/types';
import {
ButtonGroup,
Card,
Collapse,
Tooltip,
Flex,
IconTooltip,
Loading,
ModalTrigger,
type CollapseProps,
} from '@superset-ui/core/components';
import { CopyToClipboard } from 'src/components';
import { t } from '@apache-superset/core/translation';
import { styled, useTheme } from '@apache-superset/core/theme';
import { debounce } from 'lodash';
import {
removeDataPreview,
removeTables,
addDangerToast,
syncTable,
} from 'src/SqlLab/actions/sqlLab';
import {
tableApiUtil,
useTableExtendedMetadataQuery,
useTableMetadataQuery,
} from 'src/hooks/apiResources';
import useEffectEvent from 'src/hooks/useEffectEvent';
import { ActionType } from 'src/types/Action';
import { Icons } from '@superset-ui/core/components/Icons';
import { Space } from '@superset-ui/core/components/Space';
import ColumnElement, { ColumnKeyTypeType } from '../ColumnElement';
import ShowSQL from '../ShowSQL';
export interface Column {
name: string;
keys?: { type: ColumnKeyTypeType }[];
type: string;
}
export interface TableElementProps extends CollapseProps {
table: Table;
}
const StyledSpan = styled.span`
cursor: pointer;
`;
const Fade = styled.div`
transition: all ${({ theme }) => theme.motionDurationMid};
opacity: ${(props: { hovered: boolean }) => (props.hovered ? 1 : 0)};
`;
const TableElement = ({ table, ...props }: TableElementProps) => {
const { dbId, catalog, schema, name, expanded, id } = table;
const theme = useTheme();
const dispatch = useAppDispatch();
const {
currentData: tableMetadata,
isSuccess: isMetadataSuccess,
isFetching: isMetadataFetching,
isError: hasMetadataError,
} = useTableMetadataQuery(
{
dbId,
catalog,
schema,
table: name,
},
{ skip: !expanded },
);
const {
currentData: tableExtendedMetadata,
isSuccess: isExtraMetadataSuccess,
isLoading: isExtraMetadataLoading,
isError: hasExtendedMetadataError,
} = useTableExtendedMetadataQuery(
{
dbId,
catalog,
schema,
table: name,
},
{ skip: !expanded },
);
const tableData = {
...tableMetadata,
...tableExtendedMetadata,
};
const queryEditors = useSelector<SqlLabRootState, QueryEditor[]>(
state => state.sqlLab.queryEditors,
);
const currentTable = { ...tableData, ...table };
const { queryEditorId } = currentTable;
const queryEditor = queryEditors.find(
qe => qe.id === queryEditorId || qe.tabViewId === queryEditorId,
);
const currentQueryEditorId = queryEditor?.tabViewId || queryEditorId;
useEffect(() => {
if (hasMetadataError || hasExtendedMetadataError) {
dispatch(
addDangerToast(t('An error occurred while fetching table metadata')),
);
}
}, [hasMetadataError, hasExtendedMetadataError, dispatch]);
// TODO: migrate syncTable logic by SIP-93
const syncTableMetadata = useEffectEvent(() => {
const { initialized } = table;
// if not a valid number, wait for backend to assign one
const hasFinalQueryEditorId =
currentQueryEditorId &&
!Number.isNaN(Number(currentQueryEditorId)) &&
currentTable.queryEditorId !== currentQueryEditorId;
if (!initialized && hasFinalQueryEditorId) {
dispatch(syncTable(currentTable, tableData, currentQueryEditorId));
}
});
useEffect(() => {
if (isMetadataSuccess && isExtraMetadataSuccess) {
syncTableMetadata();
}
}, [
isMetadataSuccess,
isExtraMetadataSuccess,
currentQueryEditorId,
syncTableMetadata,
]);
const [sortColumns, setSortColumns] = useState(false);
const [hovered, setHovered] = useState(false);
const tableNameRef = useRef<HTMLInputElement>(null);
const setHover = (hovered: boolean) => {
debounce(() => setHovered(hovered), 100)();
};
const removeTable = () => {
dispatch(removeDataPreview(table));
dispatch(removeTables([table]));
};
const toggleSortColumns = () => {
setSortColumns(prevState => !prevState);
};
const refreshTableMetadata = () => {
dispatch(
tableApiUtil.invalidateTags([{ type: 'TableMetadatas', id: name }]),
);
dispatch(syncTable(table, tableData, table.queryEditorId));
};
const renderWell = () => {
let partitions;
let metadata;
if (tableData.partitions) {
let partitionQuery;
let partitionClipBoard;
if (tableData.partitions.partitionQuery) {
({ partitionQuery } = tableData.partitions);
const tt = t('Copy partition query to clipboard');
partitionClipBoard = (
<CopyToClipboard
text={partitionQuery}
shouldShowText={false}
tooltipText={tt}
copyNode={<Icons.CopyOutlined iconSize="s" />}
/>
);
}
const latest = Object.entries(tableData.partitions?.latest || [])
.map(([key, value]) => `${key}=${value}`)
.join('/');
partitions = (
<div>
<small>
{t('latest partition:')} {latest}
</small>{' '}
{partitionClipBoard}
</div>
);
}
if (tableData.metadata) {
metadata = Object.entries(tableData.metadata).map(([key, value]) => (
<div>
<small>
<strong>{key}:</strong> {value}
</small>
</div>
));
if (!metadata?.length) {
// hide metadata card view
return null;
}
}
if (!partitions) {
// hide partition card view
return null;
}
return (
<Card size="small">
{partitions}
{metadata}
</Card>
);
};
const renderControls = () => {
let keyLink;
const KEYS_FOR_TABLE_TEXT = t('Keys for table');
if (tableData?.indexes?.length) {
keyLink = (
<ModalTrigger
modalTitle={`${KEYS_FOR_TABLE_TEXT} ${name}`}
modalBody={tableData.indexes.map((ix, i) => (
<pre key={i}>{JSON.stringify(ix, null, ' ')}</pre>
))}
triggerNode={
<IconTooltip
className="pull-left"
tooltip={t('View keys & indexes (%s)', tableData.indexes.length)}
>
<Icons.TableOutlined
iconSize="m"
iconColor={theme.colorPrimary}
/>
</IconTooltip>
}
/>
);
}
return (
<Flex style={{ height: 22 }} align="center">
{isMetadataFetching || isExtraMetadataLoading ? (
<Loading position="inline" />
) : (
<Fade
data-test="fade"
hovered={hovered}
onClick={e => e.stopPropagation()}
>
<ButtonGroup>
<Space size="small">
<IconTooltip
className="pull-left pointer"
onClick={refreshTableMetadata}
tooltip={t('Refresh table schema')}
>
<Icons.SyncOutlined
iconSize="m"
iconColor={theme.colorIcon}
/>
</IconTooltip>
{keyLink}
<IconTooltip
onClick={toggleSortColumns}
tooltip={
sortColumns
? t('Original table column order')
: t('Sort columns alphabetically')
}
>
<Icons.SortAscendingOutlined
iconSize="m"
aria-hidden
iconColor={
sortColumns ? theme.colorIcon : theme.colorTextDisabled
}
/>
</IconTooltip>
{tableData.selectStar && (
<CopyToClipboard
copyNode={
<IconTooltip
aria-label={t('Copy')}
tooltip={t('Copy SELECT statement to the clipboard')}
>
<Icons.CopyOutlined
iconSize="m"
aria-hidden
iconColor={theme.colorIcon}
/>
</IconTooltip>
}
text={tableData.selectStar}
shouldShowText={false}
/>
)}
{tableData.view && (
<ShowSQL
sql={tableData.view}
tooltipText={t('Show CREATE VIEW statement')}
title={t('CREATE VIEW statement')}
/>
)}
<IconTooltip
className=" table-remove pull-left pointer"
onClick={removeTable}
tooltip={t('Remove table preview')}
>
<Icons.CloseOutlined
iconSize="m"
aria-hidden
iconColor={theme.colorIcon}
/>
</IconTooltip>
</Space>
</ButtonGroup>
</Fade>
)}
</Flex>
);
};
const renderHeader = () => {
const element: HTMLInputElement | null = tableNameRef.current;
let trigger = [] as ActionType[];
if (element && element.offsetWidth < element.scrollWidth) {
trigger = ['hover'];
}
return (
<div
data-test="table-element-header-container"
className="clearfix header-container"
>
<Tooltip
id="copy-to-clipboard-tooltip"
style={{ cursor: 'pointer' }}
title={name}
trigger={trigger}
>
<StyledSpan
data-test="collapse"
ref={tableNameRef}
className="table-name"
>
<strong>{name}</strong>
</StyledSpan>
</Tooltip>
</div>
);
};
const renderBody = () => {
let cols;
if (tableData.columns) {
cols = tableData.columns.slice();
if (sortColumns) {
cols.sort((a: Column, b: Column) => {
const colA = a.name.toUpperCase();
const colB = b.name.toUpperCase();
return colA < colB ? -1 : colA > colB ? 1 : 0;
});
}
}
const metadata = (
<div data-test="table-element" css={{ paddingTop: 6 }}>
{renderWell()}
<div>
{cols?.map(col => (
<ColumnElement column={col} key={col.name} />
))}
</div>
</div>
);
return metadata;
};
return (
<Collapse
activeKey={props.activeKey}
expandIconPosition="end"
onChange={props.onChange}
ghost
items={[
{
key: id,
label: renderHeader(),
children: renderBody(),
extra: renderControls(),
onMouseEnter: () => setHover(true),
onMouseLeave: () => setHover(false),
},
]}
/>
);
};
export default TableElement;

View File

@@ -98,10 +98,7 @@ class CopyToClip extends Component<CopyToClipboardProps> {
trigger={['hover']}
arrow={{ pointAtCenter: true }}
>
{/* Wrap in a span so antd Tooltip has a real DOM ref target;
avoids findDOMNode fallback when copyNode is a function
component without forwardRef. */}
<span>{this.getDecoratedCopyNode()}</span>
{this.getDecoratedCopyNode()}
</Tooltip>
) : (
this.getDecoratedCopyNode()

View File

@@ -310,7 +310,12 @@ test('ignores non-array fontUrls in theme config without throwing', () => {
});
test('skips the dashboard theme when an SDK theme config override is active', () => {
const themeConfig = { token: { colorPrimary: '#ff0000' } };
const themeConfig = {
token: {
colorPrimary: '#ff0000',
fontUrls: ['https://fonts.example.com/dashboard.css'],
},
};
render(
<ThemeContext.Provider
value={{ hasThemeConfigOverride: true } as unknown as ThemeContextType}
@@ -332,6 +337,8 @@ test('skips the dashboard theme when an SDK theme config override is active', ()
expect(
screen.queryByTestId('dashboard-theme-provider'),
).not.toBeInTheDocument();
// The override fully owns theming, so dashboard fonts must not be injected.
expect(document.querySelector('style[data-superset-fonts]')).toBeNull();
});
test('applies the dashboard theme when no SDK theme config override is active', () => {
@@ -372,52 +379,3 @@ test('does not inject font style element when no fontUrls in config', () => {
const fontStyle = document.querySelector('style[data-superset-fonts]');
expect(fontStyle).toBeNull();
});
test('prevents font injection and cleans up fonts when hasThemeConfigOverride becomes active', () => {
const fontUrl = 'https://fonts.example.com/custom.css';
const themeConfig = {
token: { colorPrimary: '#ff0000', fontUrls: [fontUrl] },
};
const { rerender } = render(
<ThemeContext.Provider
value={{ hasThemeConfigOverride: false } as unknown as ThemeContextType}
>
<CrudThemeProvider
theme={{
id: 1,
theme_name: 'Font Theme',
json_data: JSON.stringify(themeConfig),
}}
>
<div>Dashboard Content</div>
</CrudThemeProvider>
</ThemeContext.Provider>,
);
// Assert font is injected
let fontStyle = document.querySelector('style[data-superset-fonts]');
expect(fontStyle).not.toBeNull();
expect(fontStyle?.textContent).toContain(`@import url("${fontUrl}")`);
// Switch hasThemeConfigOverride dynamically to true
rerender(
<ThemeContext.Provider
value={{ hasThemeConfigOverride: true } as unknown as ThemeContextType}
>
<CrudThemeProvider
theme={{
id: 1,
theme_name: 'Font Theme',
json_data: JSON.stringify(themeConfig),
}}
>
<div>Dashboard Content</div>
</CrudThemeProvider>
</ThemeContext.Provider>,
);
// Assert font is cleaned up and removed
fontStyle = document.querySelector('style[data-superset-fonts]');
expect(fontStyle).toBeNull();
});

View File

@@ -78,9 +78,7 @@ export default function CrudThemeProvider({
}, [theme?.json_data, hasThemeConfigOverride]);
useEffect(() => {
if (hasThemeConfigOverride || !dashboardTheme || !fontUrls?.length) {
return undefined;
}
if (!dashboardTheme || !fontUrls?.length) return undefined;
// JSON.stringify provides safe escaping to prevent CSS injection
const css = fontUrls
@@ -94,7 +92,7 @@ export default function CrudThemeProvider({
return () => {
style.remove();
};
}, [dashboardTheme, fontUrls, hasThemeConfigOverride]);
}, [dashboardTheme, fontUrls]);
if (!dashboardTheme || hasThemeConfigOverride) {
return <>{children}</>;

View File

@@ -17,25 +17,21 @@
* under the License.
*/
import { sanitizeUrl } from '@braintree/sanitize-url';
import { forwardRef, PropsWithoutRef, Ref, RefAttributes } from 'react';
import { PropsWithoutRef, RefAttributes } from 'react';
import { Link, LinkProps } from 'react-router-dom';
import { isUrlExternal, parseUrl } from 'src/utils/urlUtils';
type GenericLinkProps<S> = PropsWithoutRef<LinkProps<S>> &
RefAttributes<HTMLAnchorElement>;
const GenericLinkInner = <S,>(
{ to, component, replace, innerRef, children, ...rest }: GenericLinkProps<S>,
ref: Ref<HTMLAnchorElement>,
) => {
export const GenericLink = <S,>({
to,
component,
replace,
innerRef,
children,
...rest
}: PropsWithoutRef<LinkProps<S>> & RefAttributes<HTMLAnchorElement>) => {
if (typeof to === 'string' && isUrlExternal(to)) {
return (
<a
ref={ref}
data-test="external-link"
href={sanitizeUrl(parseUrl(to))}
{...rest}
>
<a data-test="external-link" href={sanitizeUrl(parseUrl(to))} {...rest}>
{children}
</a>
);
@@ -46,14 +42,10 @@ const GenericLinkInner = <S,>(
to={to}
component={component}
replace={replace}
innerRef={innerRef ?? ref}
innerRef={innerRef}
{...rest}
>
{children}
</Link>
);
};
export const GenericLink = forwardRef(GenericLinkInner) as <S>(
props: GenericLinkProps<S> & { ref?: Ref<HTMLAnchorElement> },
) => ReturnType<typeof GenericLinkInner>;

View File

@@ -193,57 +193,52 @@ export default function getControlItemsMap({
t('Populate "Default value" to enable this control')
}
>
{/* Wrap in span so antd Tooltip can attach a ref without
relying on findDOMNode (deprecated in React 18+). */}
<span>
<StyledRowFormItem
expanded={expanded}
key={controlItem.name}
name={['filters', filterId, 'controlValues', controlItem.name]}
initialValue={initialValue}
valuePropName="checked"
colon={false}
<StyledRowFormItem
expanded={expanded}
key={controlItem.name}
name={['filters', filterId, 'controlValues', controlItem.name]}
initialValue={initialValue}
valuePropName="checked"
colon={false}
>
<Checkbox
disabled={controlItem.config.affectsDataMask && disabled}
onChange={checked => {
if (controlItem.config.requiredFirst) {
setNativeFilterFieldValues(form, filterId, {
requiredFirst: {
...formFilter?.requiredFirst,
[controlItem.name]: checked,
},
});
}
if (controlItem.config.resetConfig) {
setNativeFilterFieldValues(form, filterId, {
defaultDataMask: null,
});
}
formChanged();
forceUpdate();
}}
>
<Checkbox
disabled={controlItem.config.affectsDataMask && disabled}
onChange={checked => {
if (controlItem.config.requiredFirst) {
setNativeFilterFieldValues(form, filterId, {
requiredFirst: {
...formFilter?.requiredFirst,
[controlItem.name]: checked,
},
});
}
if (controlItem.config.resetConfig) {
setNativeFilterFieldValues(form, filterId, {
defaultDataMask: null,
});
}
formChanged();
forceUpdate();
}}
>
<>
{typeof controlItem.config.label === 'function'
? (controlItem.config.label as Function)()
: controlItem.config.label}
&nbsp;
{controlItem.config.description && (
<InfoTooltip
placement="top"
tooltip={
typeof controlItem.config.description === 'function'
? (controlItem.config.description as Function)()
: (controlItem.config
.description as React.ReactNode)
}
/>
)}
</>
</Checkbox>
</StyledRowFormItem>
</span>
<>
{typeof controlItem.config.label === 'function'
? (controlItem.config.label as Function)()
: controlItem.config.label}
&nbsp;
{controlItem.config.description && (
<InfoTooltip
placement="top"
tooltip={
typeof controlItem.config.description === 'function'
? (controlItem.config.description as Function)()
: (controlItem.config.description as React.ReactNode)
}
/>
)}
</>
</Checkbox>
</StyledRowFormItem>
</Tooltip>
</>
);

View File

@@ -54,9 +54,7 @@ const ColorBreakpointsPopoverTrigger = ({
onOpenChange={setVisibility}
destroyOnHidden
>
{/* Wrap in span so the Popover can attach a ref without relying
on findDOMNode (deprecated in React 18+). */}
<span>{props.children}</span>
{props.children}
</ControlPopover>
);
};

View File

@@ -52,9 +52,7 @@ const ContourPopoverTrigger = ({
onOpenChange={setVisibility}
destroyOnHidden
>
{/* Wrap in span so the Popover can attach a ref without relying
on findDOMNode (deprecated in React 18+). */}
<span>{props.children}</span>
{props.children}
</ControlPopover>
);
};

View File

@@ -369,21 +369,16 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
overlayClassName="time-range-popover"
>
<Tooltip placement="top" title={tooltipTitle}>
{/* Wrap in a span so the Popover gets a stable DOM ref target;
DateLabel forwards its ref to an inner span used for measuring
text truncation, which would otherwise become the popover's
positioning anchor in React 18. */}
<span data-test={DateFilterTestKey.PopoverOverlay}>
<DateLabel
name={name}
aria-labelledby={`filter-name-${props.name}`}
aria-describedby={`date-label-${props.name}`}
label={actualTimeRange}
isActive={show}
isPlaceholder={actualTimeRange === NO_TIME_RANGE}
ref={labelRef}
/>
</span>
<DateLabel
name={name}
aria-labelledby={`filter-name-${props.name}`}
aria-describedby={`date-label-${props.name}`}
label={actualTimeRange}
isActive={show}
isPlaceholder={actualTimeRange === NO_TIME_RANGE}
data-test={DateFilterTestKey.PopoverOverlay}
ref={labelRef}
/>
</Tooltip>
</ControlPopover>
);

View File

@@ -201,9 +201,7 @@ const ColumnSelectPopoverTriggerInner = ({
title={popoverTitle}
destroyOnHidden
>
{/* Wrap in span so the Popover can attach a ref without relying
on findDOMNode (deprecated in React 18+). */}
<span>{children}</span>
{children}
</ControlPopover>
</>
);

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