mirror of
https://github.com/apache/superset.git
synced 2026-06-19 14:39:20 +00:00
Compare commits
2 Commits
fix/query-
...
chore/sqla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
024aac2152 | ||
|
|
44f0667a55 |
2
.github/actions/setup-backend/action.yml
vendored
2
.github/actions/setup-backend/action.yml
vendored
@@ -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 }}
|
||||
|
||||
1
.github/workflows/codeql-analysis.yml
vendored
1
.github/workflows/codeql-analysis.yml
vendored
@@ -47,7 +47,6 @@ jobs:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
|
||||
61
.github/workflows/docker.yml
vendored
61
.github/workflows/docker.yml
vendored
@@ -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:
|
||||
|
||||
44
.github/workflows/embedded-sdk-release.yml
vendored
44
.github/workflows/embedded-sdk-release.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/generate-FOSSA-report.yml
vendored
2
.github/workflows/generate-FOSSA-report.yml
vendored
@@ -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"
|
||||
|
||||
2
.github/workflows/license-check.yml
vendored
2
.github/workflows/license-check.yml
vendored
@@ -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"
|
||||
|
||||
2
.github/workflows/superset-docs-deploy.yml
vendored
2
.github/workflows/superset-docs-deploy.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
14
UPDATING.md
14
UPDATING.md
@@ -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`.
|
||||
|
||||
@@ -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)..."
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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"
|
||||
|
||||
165
docs/yarn.lock
165
docs/yarn.lock
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ spec:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetCeleryBeat.extraContainers }}
|
||||
{{- tpl (toYaml .Values.supersetCeleryBeat.extraContainers) . | nindent 8 }}
|
||||
{{- toYaml .Values.supersetCeleryBeat.extraContainers | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector: {{- toYaml . | nindent 8 }}
|
||||
|
||||
@@ -121,7 +121,7 @@ spec:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetCeleryFlower.extraContainers }}
|
||||
{{- tpl (toYaml .Values.supersetCeleryFlower.extraContainers) . | nindent 8 }}
|
||||
{{- toYaml .Values.supersetCeleryFlower.extraContainers | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector: {{- toYaml . | nindent 8 }}
|
||||
|
||||
@@ -141,7 +141,7 @@ spec:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetWorker.extraContainers }}
|
||||
{{- tpl (toYaml .Values.supersetWorker.extraContainers) . | nindent 8 }}
|
||||
{{- toYaml .Values.supersetWorker.extraContainers | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector: {{- toYaml . | nindent 8 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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+
|
||||
|
||||
27
pytest.ini
27
pytest.ini
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
431
superset-frontend/package-lock.json
generated
431
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"cross-env": "^10.1.0",
|
||||
"fs-extra": "^11.3.5",
|
||||
"jest": "^30.4.2",
|
||||
"yeoman-test": "^11.5.3"
|
||||
"yeoman-test": "^11.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 4.0.0",
|
||||
|
||||
@@ -97,8 +97,8 @@
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*",
|
||||
"lodash": "^4.18.1",
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "*"
|
||||
},
|
||||
|
||||
@@ -17,23 +17,15 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
import { Avatar as AntdAvatar } from 'antd';
|
||||
import type { AvatarProps, GroupProps as AvatarGroupProps } from './types';
|
||||
|
||||
export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>((props, ref) => (
|
||||
<AntdAvatar ref={ref} {...props} />
|
||||
));
|
||||
export function Avatar(props: AvatarProps) {
|
||||
return <AntdAvatar {...props} />;
|
||||
}
|
||||
|
||||
// antd Avatar.Group is a plain function component without forwardRef; wrap in
|
||||
// a span so this component can be a Tooltip / Popover trigger and skip the
|
||||
// findDOMNode fallback.
|
||||
export const AvatarGroup = forwardRef<HTMLSpanElement, AvatarGroupProps>(
|
||||
(props, ref) => (
|
||||
<span ref={ref}>
|
||||
<AntdAvatar.Group {...props} />
|
||||
</span>
|
||||
),
|
||||
);
|
||||
export function AvatarGroup(props: AvatarGroupProps) {
|
||||
return <AntdAvatar.Group {...props} />;
|
||||
}
|
||||
|
||||
export type { AvatarProps, AvatarGroupProps };
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Children, ReactElement, Fragment, forwardRef, Ref } from 'react';
|
||||
import { Children, ReactElement, Fragment } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Button as AntdButton } from 'antd';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
@@ -100,7 +100,7 @@ const BUTTON_STYLE_MAP: Record<
|
||||
link: { type: 'link' },
|
||||
};
|
||||
|
||||
function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
|
||||
export function Button(props: ButtonProps) {
|
||||
const {
|
||||
tooltip,
|
||||
placement,
|
||||
@@ -160,7 +160,6 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
|
||||
|
||||
const button = (
|
||||
<AntdButton
|
||||
ref={ref as Ref<HTMLButtonElement & HTMLAnchorElement>}
|
||||
href={disabled ? undefined : href}
|
||||
disabled={disabled}
|
||||
type={antdType}
|
||||
@@ -236,6 +235,4 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
|
||||
return button;
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLElement, ButtonProps>(ButtonInner);
|
||||
|
||||
export type { ButtonProps, OnClickHandler };
|
||||
|
||||
@@ -75,10 +75,7 @@ export const DropdownButton = ({
|
||||
id={`${kebabCase(tooltip)}-tooltip`}
|
||||
title={tooltip}
|
||||
>
|
||||
{/* antd Dropdown.Button is a plain function component without
|
||||
forwardRef; wrap in a span so the Tooltip can attach a ref to a
|
||||
real DOM node and skip the findDOMNode fallback. */}
|
||||
<span>{button}</span>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,10 +240,7 @@ export function EditableTitle({
|
||||
t("You don't have the rights to alter this title.")
|
||||
}
|
||||
>
|
||||
{/* Wrap in span so the Tooltip can attach a ref to a DOM element.
|
||||
antd's Input.TextArea forwards a non-DOM imperative handle, which
|
||||
triggers a React 18 findDOMNode deprecation warning. */}
|
||||
<span>{titleComponent}</span>
|
||||
{titleComponent}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,54 +16,47 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef } from 'react';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
import { Button } from '../Button';
|
||||
import type { IconTooltipProps } from './types';
|
||||
|
||||
export const IconTooltip = forwardRef<HTMLElement, IconTooltipProps>(
|
||||
(
|
||||
{
|
||||
children = null,
|
||||
className = '',
|
||||
onClick = () => undefined,
|
||||
placement = 'top',
|
||||
style = {},
|
||||
tooltip = null,
|
||||
mouseEnterDelay = 0.3,
|
||||
mouseLeaveDelay = 0.15,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const iconTooltip = (
|
||||
<Button
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
padding: 0,
|
||||
...style,
|
||||
}}
|
||||
buttonStyle="link"
|
||||
className={`IconTooltip ${className}`}
|
||||
export const IconTooltip = ({
|
||||
children = null,
|
||||
className = '',
|
||||
onClick = () => undefined,
|
||||
placement = 'top',
|
||||
style = {},
|
||||
tooltip = null,
|
||||
mouseEnterDelay = 0.3,
|
||||
mouseLeaveDelay = 0.15,
|
||||
}: IconTooltipProps) => {
|
||||
const iconTooltip = (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
style={{
|
||||
padding: 0,
|
||||
...style,
|
||||
}}
|
||||
buttonStyle="link"
|
||||
className={`IconTooltip ${className}`}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
id="tooltip"
|
||||
title={tooltip}
|
||||
placement={placement}
|
||||
mouseEnterDelay={mouseEnterDelay}
|
||||
mouseLeaveDelay={mouseLeaveDelay}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
{iconTooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
id="tooltip"
|
||||
title={tooltip}
|
||||
placement={placement}
|
||||
mouseEnterDelay={mouseEnterDelay}
|
||||
mouseLeaveDelay={mouseLeaveDelay}
|
||||
>
|
||||
{iconTooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return iconTooltip;
|
||||
},
|
||||
);
|
||||
}
|
||||
return iconTooltip;
|
||||
};
|
||||
|
||||
export type { IconTooltipProps };
|
||||
|
||||
@@ -165,7 +165,7 @@ import {
|
||||
SlackOutlined,
|
||||
ApiOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { ForwardRefExoticComponent, RefAttributes, forwardRef } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { IconType } from './types';
|
||||
import { BaseIconComponent } from './BaseIcon';
|
||||
|
||||
@@ -323,25 +323,19 @@ type AntdIconNames = keyof typeof AntdIcons;
|
||||
|
||||
export const antdEnhancedIcons: Record<
|
||||
AntdIconNames,
|
||||
ForwardRefExoticComponent<IconType & RefAttributes<HTMLSpanElement>>
|
||||
FC<IconType>
|
||||
> = Object.keys(AntdIcons)
|
||||
.filter(key => !EXCLUDED_ICONS.some(excluded => key.includes(excluded)))
|
||||
.reduce(
|
||||
(acc, key) => {
|
||||
acc[key as AntdIconNames] = forwardRef<HTMLSpanElement, IconType>(
|
||||
(props, ref) => (
|
||||
<BaseIconComponent
|
||||
ref={ref}
|
||||
component={AntdIcons[key as AntdIconNames]}
|
||||
fileName={key}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
acc[key as AntdIconNames] = (props: IconType) => (
|
||||
<BaseIconComponent
|
||||
component={AntdIcons[key as AntdIconNames]}
|
||||
fileName={key}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<
|
||||
AntdIconNames,
|
||||
ForwardRefExoticComponent<IconType & RefAttributes<HTMLSpanElement>>
|
||||
>,
|
||||
{} as Record<AntdIconNames, FC<IconType>>,
|
||||
);
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { FC, SVGProps, forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import { FC, SVGProps, useEffect, useRef, useState } from 'react';
|
||||
import TransparentIcon from './svgs/transparent.svg';
|
||||
import { IconType } from './types';
|
||||
import { BaseIconComponent } from './BaseIcon';
|
||||
|
||||
const AsyncIcon = forwardRef<HTMLSpanElement, IconType>((props, ref) => {
|
||||
const AsyncIcon = (props: IconType) => {
|
||||
const [, setLoaded] = useState(false);
|
||||
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
|
||||
const { fileName, customIcons, iconSize, iconColor, viewBox, ...restProps } =
|
||||
@@ -46,7 +46,6 @@ const AsyncIcon = forwardRef<HTMLSpanElement, IconType>((props, ref) => {
|
||||
|
||||
return (
|
||||
<BaseIconComponent
|
||||
ref={ref}
|
||||
component={ImportedSVG.current || TransparentIcon}
|
||||
fileName={fileName}
|
||||
customIcons={customIcons}
|
||||
@@ -56,6 +55,6 @@ const AsyncIcon = forwardRef<HTMLSpanElement, IconType>((props, ref) => {
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default AsyncIcon;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef, type ComponentType } from 'react';
|
||||
import { css, useTheme, getFontSize } from '@apache-superset/core/theme';
|
||||
import { AntdIconType, BaseIconProps, CustomIconType, IconType } from './types';
|
||||
|
||||
@@ -36,78 +35,65 @@ const genAriaLabel = (fileName: string) => {
|
||||
return name.toLowerCase();
|
||||
};
|
||||
|
||||
export const BaseIconComponent = forwardRef<
|
||||
HTMLSpanElement | SVGSVGElement,
|
||||
export const BaseIconComponent: React.FC<
|
||||
BaseIconProps & Omit<IconType, 'component'>
|
||||
>(
|
||||
(
|
||||
{
|
||||
component: Component,
|
||||
iconColor,
|
||||
iconSize,
|
||||
viewBox,
|
||||
customIcons,
|
||||
fileName,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const whatRole = rest?.onClick ? 'button' : 'img';
|
||||
const ariaLabel = genAriaLabel(fileName || '');
|
||||
const style = {
|
||||
color: iconColor,
|
||||
fontSize: iconSize
|
||||
? `${getFontSize(theme, iconSize)}px`
|
||||
: `${theme.fontSize}px`,
|
||||
cursor: rest?.onClick ? 'pointer' : undefined,
|
||||
};
|
||||
> = ({
|
||||
component: Component,
|
||||
iconColor,
|
||||
iconSize,
|
||||
viewBox,
|
||||
customIcons,
|
||||
fileName,
|
||||
...rest
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const whatRole = rest?.onClick ? 'button' : 'img';
|
||||
const ariaLabel = genAriaLabel(fileName || '');
|
||||
const style = {
|
||||
color: iconColor,
|
||||
fontSize: iconSize
|
||||
? `${getFontSize(theme, iconSize)}px`
|
||||
: `${theme.fontSize}px`,
|
||||
cursor: rest?.onClick ? 'pointer' : undefined,
|
||||
};
|
||||
|
||||
const AntdComponent = Component as ComponentType<
|
||||
Record<string, unknown> & {
|
||||
ref?: React.Ref<HTMLSpanElement | SVGSVGElement>;
|
||||
}
|
||||
>;
|
||||
return customIcons ? (
|
||||
<span
|
||||
ref={ref as React.Ref<HTMLSpanElement>}
|
||||
role={whatRole}
|
||||
aria-label={ariaLabel}
|
||||
data-test={ariaLabel}
|
||||
css={[
|
||||
css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 0;
|
||||
vertical-align: middle;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<Component
|
||||
viewBox={viewBox || '0 0 24 24'}
|
||||
style={style}
|
||||
width={
|
||||
iconSize
|
||||
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
|
||||
: `${theme.fontSize}px`
|
||||
}
|
||||
height={
|
||||
iconSize
|
||||
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
|
||||
: `${theme.fontSize}px`
|
||||
}
|
||||
{...(rest as CustomIconType)}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<AntdComponent
|
||||
ref={ref}
|
||||
role={whatRole}
|
||||
return customIcons ? (
|
||||
<span
|
||||
role={whatRole}
|
||||
aria-label={ariaLabel}
|
||||
data-test={ariaLabel}
|
||||
css={[
|
||||
css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 0;
|
||||
vertical-align: middle;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<Component
|
||||
viewBox={viewBox || '0 0 24 24'}
|
||||
style={style}
|
||||
aria-label={ariaLabel}
|
||||
data-test={ariaLabel}
|
||||
{...(rest as AntdIconType)}
|
||||
width={
|
||||
iconSize
|
||||
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
|
||||
: `${theme.fontSize}px`
|
||||
}
|
||||
height={
|
||||
iconSize
|
||||
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
|
||||
: `${theme.fontSize}px`
|
||||
}
|
||||
{...(rest as CustomIconType)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
</span>
|
||||
) : (
|
||||
<Component
|
||||
role={whatRole}
|
||||
style={style}
|
||||
aria-label={ariaLabel}
|
||||
data-test={ariaLabel}
|
||||
{...(rest as AntdIconType)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,16 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ForwardRefExoticComponent, RefAttributes, forwardRef } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { antdEnhancedIcons } from './AntdEnhanced';
|
||||
import AsyncIcon from './AsyncIcon';
|
||||
|
||||
import type { IconType } from './types';
|
||||
|
||||
type IconComponent = ForwardRefExoticComponent<
|
||||
IconType & RefAttributes<HTMLSpanElement>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Filename is going to be inferred from the icon name.
|
||||
* i.e. BigNumberChartTile => assets/images/icons/big_number_chart_tile
|
||||
@@ -62,17 +58,15 @@ const customIcons = [
|
||||
'Undo',
|
||||
] as const;
|
||||
|
||||
type CustomIconType = Record<(typeof customIcons)[number], IconComponent>;
|
||||
type CustomIconType = Record<(typeof customIcons)[number], FC<IconType>>;
|
||||
|
||||
const iconOverrides: CustomIconType = {} as CustomIconType;
|
||||
customIcons.forEach(customIcon => {
|
||||
const fileName = customIcon
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
||||
.toLowerCase();
|
||||
iconOverrides[customIcon] = forwardRef<HTMLSpanElement, IconType>(
|
||||
(props, ref) => (
|
||||
<AsyncIcon ref={ref} customIcons fileName={fileName} {...props} />
|
||||
),
|
||||
iconOverrides[customIcon] = (props: IconType) => (
|
||||
<AsyncIcon customIcons fileName={fileName} {...props} />
|
||||
);
|
||||
});
|
||||
|
||||
@@ -80,7 +74,7 @@ export type IconNameType =
|
||||
| keyof typeof antdEnhancedIcons
|
||||
| keyof typeof iconOverrides;
|
||||
|
||||
type IconComponentType = Record<IconNameType, IconComponent>;
|
||||
type IconComponentType = Record<IconNameType, FC<IconType>>;
|
||||
|
||||
export const Icons: IconComponentType = {
|
||||
...antdEnhancedIcons,
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef } from 'react';
|
||||
import { Tag } from '@superset-ui/core/components/Tag';
|
||||
import { css } from '@emotion/react';
|
||||
import { useTheme, getColorVariants } from '@apache-superset/core/theme';
|
||||
@@ -24,7 +23,7 @@ import { DatasetTypeLabel } from './reusable/DatasetTypeLabel';
|
||||
import { PublishedLabel } from './reusable/PublishedLabel';
|
||||
import type { LabelProps } from './types';
|
||||
|
||||
export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
|
||||
export function Label(props: LabelProps) {
|
||||
const theme = useTheme();
|
||||
// Use Ant Design's motion duration instead of deprecated transitionTiming
|
||||
const {
|
||||
@@ -72,7 +71,6 @@ export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
|
||||
|
||||
return (
|
||||
<Tag
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
role={onClick ? 'button' : undefined}
|
||||
style={style}
|
||||
@@ -83,6 +81,6 @@ export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
}
|
||||
export { DatasetTypeLabel, PublishedLabel };
|
||||
export type { LabelType } from './types';
|
||||
|
||||
@@ -371,9 +371,6 @@ const CustomModal = ({
|
||||
disabled={!draggable || dragDisabled}
|
||||
bounds={bounds ?? false}
|
||||
onStart={(event, uiData) => onDragStart(event, uiData)}
|
||||
// Pass nodeRef so react-draggable does not fall back to
|
||||
// ReactDOM.findDOMNode (deprecated in React 18+ Strict Mode).
|
||||
nodeRef={draggableRef}
|
||||
{...draggableConfig}
|
||||
>
|
||||
{resizable ? (
|
||||
|
||||
@@ -16,15 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef } from 'react';
|
||||
import { Popover as AntdPopover } from 'antd';
|
||||
import { PopoverProps as AntdPopoverProps } from 'antd/es/popover';
|
||||
import type { TooltipRef } from 'antd/es/tooltip';
|
||||
|
||||
export interface PopoverProps extends AntdPopoverProps {
|
||||
forceRender?: boolean;
|
||||
}
|
||||
|
||||
export const Popover = forwardRef<TooltipRef, PopoverProps>((props, ref) => (
|
||||
<AntdPopover ref={ref} {...props} />
|
||||
));
|
||||
export const Popover = (props: PopoverProps) => <AntdPopover {...props} />;
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { MouseEventHandler } from 'react';
|
||||
import { MouseEventHandler, forwardRef } from 'react';
|
||||
import { SupersetTheme } from '@apache-superset/core/theme';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import type { IconType } from '@superset-ui/core/components/Icons/types';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
|
||||
export interface RefreshLabelProps {
|
||||
@@ -31,19 +32,25 @@ const RefreshLabel = ({
|
||||
onClick,
|
||||
tooltipContent,
|
||||
disabled,
|
||||
}: RefreshLabelProps) => (
|
||||
<Tooltip title={tooltipContent}>
|
||||
<Icons.SyncOutlined
|
||||
iconSize="l"
|
||||
role="button"
|
||||
onClick={disabled ? undefined : onClick}
|
||||
css={(theme: SupersetTheme) => ({
|
||||
cursor: 'pointer',
|
||||
color: theme.colorIcon,
|
||||
'&:hover': { color: theme.colorPrimary },
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}: RefreshLabelProps) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const IconWithoutRef = forwardRef((props: IconType, ref: any) => (
|
||||
<Icons.SyncOutlined iconSize="l" {...props} />
|
||||
));
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltipContent}>
|
||||
<IconWithoutRef
|
||||
role="button"
|
||||
onClick={disabled ? undefined : onClick}
|
||||
css={(theme: SupersetTheme) => ({
|
||||
cursor: 'pointer',
|
||||
color: theme.colorIcon,
|
||||
'&:hover': { color: theme.colorPrimary },
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default RefreshLabel;
|
||||
|
||||
@@ -16,22 +16,17 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef } from 'react';
|
||||
import { Tooltip as AntdTooltip } from 'antd';
|
||||
import type { TooltipRef } from 'antd/es/tooltip';
|
||||
|
||||
import type { TooltipProps, TooltipPlacement } from './types';
|
||||
|
||||
export const Tooltip = forwardRef<TooltipRef, TooltipProps>(
|
||||
({ overlayStyle, ...props }, ref) => (
|
||||
<AntdTooltip
|
||||
ref={ref}
|
||||
styles={{
|
||||
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
root: overlayStyle ?? {},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
export const Tooltip = ({ overlayStyle, ...props }: TooltipProps) => (
|
||||
<AntdTooltip
|
||||
styles={{
|
||||
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
root: overlayStyle ?? {},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
export type { TooltipProps, TooltipPlacement };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0"
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -31,7 +31,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0"
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0"
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -36,6 +36,6 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0"
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0"
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -39,6 +39,6 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0"
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.18.1",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"dompurify": "^3.4.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"ol": "^10.8.0",
|
||||
"polished": "*",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"dayjs": "^1.11.21",
|
||||
"echarts": "*",
|
||||
"memoize-one": "*",
|
||||
"react": "^18.3.0"
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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 || '');
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' }),
|
||||
);
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
420
superset-frontend/src/SqlLab/components/TableElement/index.tsx
Normal file
420
superset-frontend/src/SqlLab/components/TableElement/index.tsx
Normal 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;
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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}</>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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}
|
||||
|
||||
{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}
|
||||
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user