mirror of
https://github.com/apache/superset.git
synced 2026-06-18 14:09:16 +00:00
Compare commits
32 Commits
enxdev/cha
...
docs/fix-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
014d6acd9a | ||
|
|
883b7a286d | ||
|
|
d9d8b2bcc0 | ||
|
|
9da54eff84 | ||
|
|
fb2b9fa8ff | ||
|
|
31797005db | ||
|
|
ca2d340db3 | ||
|
|
ef82da8458 | ||
|
|
fee1cf9f08 | ||
|
|
2d2a8f3ab0 | ||
|
|
a19093e65a | ||
|
|
b72a0a53c0 | ||
|
|
512b6f43c1 | ||
|
|
b18fab7fc1 | ||
|
|
b06c6b7464 | ||
|
|
bede4b2121 | ||
|
|
5e812c8757 | ||
|
|
de390f22a4 | ||
|
|
464c67d586 | ||
|
|
7f7f87e823 | ||
|
|
7c2f5142ce | ||
|
|
874ac3dc01 | ||
|
|
f56e34d6e6 | ||
|
|
742a21f6f7 | ||
|
|
a7c49ac9f2 | ||
|
|
99d927eac7 | ||
|
|
994594e4a8 | ||
|
|
e92599fb50 | ||
|
|
eebe1a1a5b | ||
|
|
664e777a84 | ||
|
|
750518cf6f | ||
|
|
59d1b5f300 |
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@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: ${{ steps.set-python-version.outputs.python-version }}
|
||||
cache: ${{ inputs.cache }}
|
||||
|
||||
61
.github/workflows/docker.yml
vendored
61
.github/workflows/docker.yml
vendored
@@ -75,6 +75,24 @@ 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:
|
||||
@@ -101,13 +119,27 @@ jobs:
|
||||
PUSH_OR_LOAD="--load"
|
||||
fi
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
# in the context of push (using multi-platform build), we need to pull the image locally
|
||||
- name: Docker pull
|
||||
@@ -148,6 +180,21 @@ 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:
|
||||
|
||||
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
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
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
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
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
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "21"
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# 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,6 +1,11 @@
|
||||
# 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,6 +396,10 @@ 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"]
|
||||
|
||||
10
UPDATING.md
10
UPDATING.md
@@ -24,6 +24,16 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### `thumbnail_url` removed from dashboard list API response
|
||||
|
||||
The `thumbnail_url` field has been removed from `GET /api/v1/dashboard/` list responses. External consumers relying on this field must now construct the thumbnail URL client-side using `id` and `changed_on_utc`:
|
||||
|
||||
```
|
||||
/api/v1/dashboard/{id}/thumbnail/{changed_on_utc}/
|
||||
```
|
||||
|
||||
The thumbnail endpoint redirects to the current digest URL regardless of whether the supplied digest is exact. If the image is not yet cached, that digest URL may return `202` and trigger async generation. Using `changed_on_utc` as the digest is sufficient for cache-busting purposes.
|
||||
|
||||
### Webhook alerts/reports block private/internal hosts by default
|
||||
|
||||
Webhook alert/report dispatch (`WebhookNotification.send`) now validates the target URL's host against the same private/internal-IP block applied to dataset import URLs. If the resolved host is in a loopback, link-local, private (RFC-1918), shared-CGNAT, or multicast range, the webhook is rejected with `NotificationParamException`.
|
||||
|
||||
@@ -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}
|
||||
celery --app=superset.tasks.celery_app:app worker -O fair -l INFO --concurrency=${CELERYD_CONCURRENCY:-2} ${WORKER_LOG_FILE:+--logfile=$WORKER_LOG_FILE}
|
||||
;;
|
||||
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
|
||||
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}
|
||||
;;
|
||||
app)
|
||||
echo "Starting web app (using development server)..."
|
||||
|
||||
@@ -22,31 +22,24 @@ level dependencies.
|
||||
|
||||
**Debian and Ubuntu**
|
||||
|
||||
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 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
|
||||
```
|
||||
|
||||
In Ubuntu **20.04 and 22.04** the following command will ensure that the required dependencies are installed:
|
||||
The following command will ensure that the required dependencies are installed (tested on Ubuntu 20.04, 22.04, and 24.04):
|
||||
|
||||
```bash
|
||||
sudo apt-get install build-essential libssl-dev libffi-dev python3-dev python3-pip libsasl2-dev libldap2-dev default-libmysqlclient-dev
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
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.
|
||||
|
||||
**Fedora and RHEL-derivative Linux distributions**
|
||||
|
||||
Install the following packages using the `yum` package manager:
|
||||
|
||||
```bash
|
||||
sudo yum install gcc gcc-c++ libffi-devel python-devel python-pip python-wheel openssl-devel cyrus-sasl-devel openldap-devel
|
||||
sudo yum install gcc gcc-c++ libffi-devel python3-devel python3-pip python3-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.40",
|
||||
"@swc/core": "^1.15.41",
|
||||
"antd": "^6.4.3",
|
||||
"baseline-browser-mapping": "^2.10.34",
|
||||
"baseline-browser-mapping": "^2.10.35",
|
||||
"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.3",
|
||||
"prettier": "^3.8.4",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.61.0",
|
||||
"webpack": "^5.107.2"
|
||||
|
||||
150
docs/yarn.lock
150
docs/yarn.lock
@@ -4143,86 +4143,86 @@
|
||||
dependencies:
|
||||
apg-lite "^1.0.4"
|
||||
|
||||
"@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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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@^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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@swc/counter" "^0.1.3"
|
||||
"@swc/types" "^0.1.26"
|
||||
optionalDependencies:
|
||||
"@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/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/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.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==
|
||||
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==
|
||||
|
||||
batch@0.6.1:
|
||||
version "0.6.1"
|
||||
@@ -12270,10 +12270,10 @@ prettier-linter-helpers@^1.0.1:
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
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==
|
||||
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==
|
||||
|
||||
pretty-error@^4.0.0:
|
||||
version "4.0.0"
|
||||
@@ -15006,9 +15006,9 @@ webpack-dev-middleware@^7.4.2:
|
||||
schema-utils "^4.0.0"
|
||||
|
||||
webpack-dev-server@^5.2.2:
|
||||
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==
|
||||
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==
|
||||
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.1 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.16.2 # 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 }}
|
||||
{{- toYaml .Values.supersetCeleryBeat.extraContainers | nindent 8 }}
|
||||
{{- tpl (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 }}
|
||||
{{- toYaml .Values.supersetCeleryFlower.extraContainers | nindent 8 }}
|
||||
{{- tpl (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 }}
|
||||
{{- toYaml .Values.supersetWorker.extraContainers | nindent 8 }}
|
||||
{{- tpl (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 }}
|
||||
{{- toYaml .Values.supersetWebsockets.extraContainers | nindent 8 }}
|
||||
{{- tpl (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 }}
|
||||
{{- toYaml .Values.supersetNode.extraContainers | nindent 8 }}
|
||||
{{- tpl (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 }}
|
||||
{{- toYaml .Values.init.extraContainers | nindent 6 }}
|
||||
{{- tpl (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>=42.0.4, <47.0.0",
|
||||
"cryptography>=48.0.0, <49.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <4.0.0",
|
||||
"flask-appbuilder>=5.2.1, <6.0.0",
|
||||
|
||||
27
pytest.ini
27
pytest.ini
@@ -18,5 +18,30 @@
|
||||
testpaths =
|
||||
tests
|
||||
python_files = *_test.py test_*.py *_tests.py *viz/utils.py
|
||||
addopts = -p no:warnings
|
||||
# `-p no:warnings` temporarily disabled in favor of more finely tuned `filterwarnings`.
|
||||
#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>=46.0.7,<47.0.0
|
||||
cryptography>=48.0.0,<49.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==46.0.7
|
||||
cryptography==48.0.1
|
||||
# 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.0.0
|
||||
pyopenssl==26.2.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# shillelagh
|
||||
@@ -344,7 +344,6 @@ 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==46.0.7
|
||||
cryptography==48.0.1
|
||||
# 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.0.0
|
||||
pyopenssl==26.2.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# shillelagh
|
||||
@@ -841,7 +841,6 @@ 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.3.0",
|
||||
"version": "0.4.0",
|
||||
"description": "SDK for embedding resources from Superset into your own application",
|
||||
"access": "public",
|
||||
"keywords": [
|
||||
|
||||
@@ -107,7 +107,13 @@ module.exports = {
|
||||
[
|
||||
'babel-plugin-jsx-remove-data-test-id',
|
||||
{
|
||||
attributes: 'data-test',
|
||||
// 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',
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
361
superset-frontend/package-lock.json
generated
361
superset-frontend/package-lock.json
generated
@@ -120,13 +120,13 @@
|
||||
"ol": "^10.9.0",
|
||||
"query-string": "9.4.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.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.2.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"react-json-tree": "^0.20.0",
|
||||
@@ -182,12 +182,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.2",
|
||||
"@storybook/addon-links": "10.4.2",
|
||||
"@storybook/react-webpack5": "10.4.2",
|
||||
"@storybook/addon-docs": "10.4.3",
|
||||
"@storybook/addon-links": "10.4.3",
|
||||
"@storybook/react-webpack5": "10.4.3",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.40",
|
||||
"@swc/core": "^1.15.41",
|
||||
"@swc/plugin-emotion": "^14.12.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
@@ -202,8 +202,8 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^25.9.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
@@ -263,16 +263,17 @@
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.69.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.8.3",
|
||||
"prettier": "3.8.4",
|
||||
"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.2",
|
||||
"storybook": "10.4.3",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
@@ -286,7 +287,7 @@
|
||||
"webpack": "^5.107.2",
|
||||
"webpack-bundle-analyzer": "^5.3.0",
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.4",
|
||||
"webpack-dev-server": "^5.2.5",
|
||||
"webpack-manifest-plugin": "^6.0.1",
|
||||
"webpack-sources": "^3.5.0",
|
||||
"webpack-visualizer-plugin2": "^2.0.0"
|
||||
@@ -9728,16 +9729,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.2.tgz",
|
||||
"integrity": "sha512-CtW1O4xSKZPNtpWgpfp4yB/x4pj/of+3MvlEDfErSlr3Hp3QmEa2pCLaecR08H5LJqJFlt1PtG0UrIynTvgW9w==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.3.tgz",
|
||||
"integrity": "sha512-CJGEXSo0zpIy7gvEeeUi09ZbjQUSNDi4YipAeb+lZGGEn8ShZUr2Pk330yd2ZO+ngNWJXD4ZxOb0e3/aIlxb3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.4.2",
|
||||
"@storybook/csf-plugin": "10.4.3",
|
||||
"@storybook/icons": "^2.0.2",
|
||||
"@storybook/react-dom-shim": "10.4.2",
|
||||
"@storybook/react-dom-shim": "10.4.3",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
@@ -9748,7 +9749,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.2"
|
||||
"storybook": "^10.4.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -9757,9 +9758,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.2.tgz",
|
||||
"integrity": "sha512-cU8h4/m+oAr8UUwF4teZG2N1ilV+vU+98Ii/Ma+IIx9M/V7i5544UxfAz84dV5Rx2Oho6x8XH3gIvmevSyPi/Q==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.3.tgz",
|
||||
"integrity": "sha512-GBJz1cwnhtwltu9KGUudceAWGFl0SRBv6r/MVc1FuD6ygpi/a8+tCc8TiEzFLFBJaU+4AgJ8IGtXSPzPihH2Iw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9772,7 +9773,7 @@
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.2"
|
||||
"storybook": "^10.4.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -9784,13 +9785,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-webpack5": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.2.tgz",
|
||||
"integrity": "sha512-nhmV0+nThCgy1y5742SS7c4vJrd5/1KfCXCNfsJ1v4Rkq7NIQnUhEIBwkSaY63lqH7FRHlFxIjwGS63veiCJuw==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.3.tgz",
|
||||
"integrity": "sha512-IfBz50mA47fUP+GrcchnE5ReOWHiUtpNi0dEJ+eCU7wbSYwxwsvmBuk9xNwfsSDZ0LG1qGFQ6Pyt6bHn15wWXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.4.2",
|
||||
"@storybook/core-webpack": "10.4.3",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||
"cjs-module-lexer": "^1.2.3",
|
||||
"css-loader": "^7.1.2",
|
||||
@@ -9811,7 +9812,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.2"
|
||||
"storybook": "^10.4.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -9820,9 +9821,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core-webpack": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.2.tgz",
|
||||
"integrity": "sha512-qnYKMruU8lvI4yaq2PA9Gmxjrc7EZ3DRBI/cVKwEgOIREoxzr1F1IE7t7+325k9Phylue7E5rD3A7yjxeEKUyw==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.3.tgz",
|
||||
"integrity": "sha512-8bLxUnajwfLXSvaX3LoGy4g/M/SZkU3tzc2lFB+FZukxmlEcyWBEjfM910muu2kuaQULP4NUNR+4LGJpuZoYoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9833,13 +9834,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.2"
|
||||
"storybook": "^10.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.2.tgz",
|
||||
"integrity": "sha512-GqX/2DeF3/jKs5D7gpDiuT9gd0c/f2TKcnQ5av4/s3YqeN+0nhm7btkCrDfgF16uzE1Zj3OrkxvB3AOkfxWgDg==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.3.tgz",
|
||||
"integrity": "sha512-D+XF5CVhZmIOI0uhfTKxlQr+gR1z8X9djPy9phiA1USLPAOHagBAucp/PhLwlFVUxrKzEIf8yImrvkCv50IcDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9852,7 +9853,7 @@
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "^10.4.2",
|
||||
"storybook": "^10.4.3",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
@@ -9890,13 +9891,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preset-react-webpack": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.2.tgz",
|
||||
"integrity": "sha512-21ld380f0/jTTitkfhTKgP3FBnVAgMu1P1ymrRyiFYJVSJBA5YejndFFBo0ugq9iGGsHXrVdOphC/OJKbTSWRQ==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.3.tgz",
|
||||
"integrity": "sha512-814EDY4yMTVECLoIFeCdbpoRuaFxtvN/U5TcB6czb6SDabaagEHsXWWVIDhx1xZlBGK6KUrxD9CMACPcB2fcKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.4.2",
|
||||
"@storybook/core-webpack": "10.4.3",
|
||||
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"magic-string": "^0.30.5",
|
||||
@@ -9913,7 +9914,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.2"
|
||||
"storybook": "^10.4.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -9922,14 +9923,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.2.tgz",
|
||||
"integrity": "sha512-NfEH3CrdCAgUV4Z7SPN3Iw6nofcueqtRj8iHuo77GNjz0qSfuVi9iS7a8o7x7QFSeIBZwS0Jv3CgmhN8qvoLjg==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.3.tgz",
|
||||
"integrity": "sha512-Td+Zoi8ylJTPC1jg5vHw8OK7U2kJgqc5kuAn92UvD4IbAkcpMTBRPHDziK1piv6q7r8yNLVah+ku6IKHpTLeXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "10.4.2",
|
||||
"@storybook/react-dom-shim": "10.4.3",
|
||||
"react-docgen": "^8.0.2",
|
||||
"react-docgen-typescript": "^2.2.2"
|
||||
},
|
||||
@@ -9942,7 +9943,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.2",
|
||||
"storybook": "^10.4.3",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -10022,9 +10023,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.2.tgz",
|
||||
"integrity": "sha512-Eng3Yt2NCjPX94QcfyLeUFhrMj0hec2yU9J/qafBVbfj9XrFI8o+0ZwYJ7uXb9ECbvPN4y06dgt/2W/LiR417w==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.3.tgz",
|
||||
"integrity": "sha512-aPZ+Afd+zdoSygISOVCJIYdiqWJM6uRZFw0zhsONwNcn3kU1TNce6iAoBCY8cpEhDAu61M1QjeIHM3LPy/ieog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -10036,7 +10037,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.2"
|
||||
"storybook": "^10.4.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -10048,15 +10049,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.4.2.tgz",
|
||||
"integrity": "sha512-x7xwGLxU0w6/qi29/cHhua8qiCvfE05ku4pPLTXF8TsP/zfGsY8tbdlKO2+YKp+iBG8vafVc//ZXOAty1oypDA==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.4.3.tgz",
|
||||
"integrity": "sha512-eWcXmRPixExctjT0Z6e5+efYemGfD953L0XkYcHaEXzlQYudZgIpbemknzFmyhkHG32F5DDgL4L5iBuL5A75Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/builder-webpack5": "10.4.2",
|
||||
"@storybook/preset-react-webpack": "10.4.2",
|
||||
"@storybook/react": "10.4.2"
|
||||
"@storybook/builder-webpack5": "10.4.3",
|
||||
"@storybook/preset-react-webpack": "10.4.3",
|
||||
"@storybook/react": "10.4.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -10065,7 +10066,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.2",
|
||||
"storybook": "^10.4.3",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -10499,9 +10500,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.40.tgz",
|
||||
"integrity": "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.41.tgz",
|
||||
"integrity": "sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
@@ -10517,18 +10518,18 @@
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@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/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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": ">=0.5.17"
|
||||
@@ -10540,9 +10541,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz",
|
||||
"integrity": "sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.41.tgz",
|
||||
"integrity": "sha512-kREh6J5paQFvP3i7f/4FbqRNOJREutVFVOkder4GVyCBQ39YmER55cW/y1NNjwrchzFqgYswFn0mMDCqbqKzrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10556,9 +10557,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz",
|
||||
"integrity": "sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.41.tgz",
|
||||
"integrity": "sha512-N8B56ESFazZAWZyIkecADSPCwlLEinW7QLMEeotCpv4J7VXwfH+OLkmRL8o96UZ+1355fwHxDTS6/wK7yucvkA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -10572,9 +10573,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz",
|
||||
"integrity": "sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.41.tgz",
|
||||
"integrity": "sha512-6XrId2fyle0mS5xxON8rU84mPd2Cq1kDJRj+4BnQKTd7u+2kSA6Ww+JkOP0iTNqOqt9OXhPOEAjBHAuonWcdCg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -10588,12 +10589,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz",
|
||||
"integrity": "sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.41.tgz",
|
||||
"integrity": "sha512-ynLIarxlkVnqHn1D0fKOVht6mNU5ks6lrH+MY3kkS+XFaGGgDxFZVjWKJlkYTKm3RCvBTfA8Ng5fLufXheMRKQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10604,12 +10608,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz",
|
||||
"integrity": "sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.41.tgz",
|
||||
"integrity": "sha512-dXu/5vd4gh8symyhRF+4G7gOPkjmb4pONhh7sl+6GSiW0LOKZlfu5kXmyFbTz9smOT7jgr002qY9b1nujjXt2A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10620,12 +10627,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-ppc64-gnu": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz",
|
||||
"integrity": "sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.41.tgz",
|
||||
"integrity": "sha512-XGO6zVPXoPE0gf/XnI4jBbafNT13AYgoh6ns0JCSdOetI/kqVf0vhpz7NuNgAzZrMVCsmieqjPoTwViDgh4mOQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10636,12 +10646,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-s390x-gnu": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz",
|
||||
"integrity": "sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.41.tgz",
|
||||
"integrity": "sha512-0WUglRwyZtW+iMi7J3iFdrCxreZZIKf4egTwEQfIYRsqFax69A0OrFj+NIoFSE03xBT/IFRrg+S8K6f9Ky+4hA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10652,12 +10665,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz",
|
||||
"integrity": "sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.41.tgz",
|
||||
"integrity": "sha512-VxkuQK59c0tHm6uJZCUrS3cyA2JhGGfdU6e41SZz0x/JS+4Sm7C1mIc97In14vkZJopEt7yXA2TouCqZDSygEA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10668,12 +10684,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz",
|
||||
"integrity": "sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.41.tgz",
|
||||
"integrity": "sha512-/0qXIu1ZxggLuovLb22vFfKHq2AA4n6Whw5UwmVCHk4pkw7KWnPIQpMCEqUMPsNkFJig7PPp/TSYFu8ZEb2rtQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10684,9 +10703,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz",
|
||||
"integrity": "sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.41.tgz",
|
||||
"integrity": "sha512-Y481sMNZM6rECh9VO4+y26N1lWEDAyxnBZskUf37fl90uHE946VHfmiVQWT0uMFOhyJJFovGTRuF4W82dwewUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10700,9 +10719,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz",
|
||||
"integrity": "sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.41.tgz",
|
||||
"integrity": "sha512-BAchBD5qeUzy3hiPSLJtaaoSm4blCLyYffOF1bGE4ETcV+OisqjUAwDQMJj++4bTpvMCDzwC+Bj3PmQyBCtscw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -10716,9 +10735,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz",
|
||||
"integrity": "sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==",
|
||||
"version": "1.15.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.41.tgz",
|
||||
"integrity": "sha512-WOkA+fJ/ViVBQDsSV9JC52NACTe5PhlurA6viASDZGb7HR3KS01ZG7RZ+Bg6SVQFIoq3gSbTsskQVe6EbHFAYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -26108,21 +26127,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/@noble/hashes": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
@@ -34200,9 +34204,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
||||
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
||||
"version": "3.8.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz",
|
||||
"integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -35641,6 +35645,16 @@
|
||||
"dnd-core": "^11.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dnd-test-backend": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-test-backend/-/react-dnd-test-backend-11.1.3.tgz",
|
||||
"integrity": "sha512-5qFm+NI2GdWIUfiYun0A8Gv0xjbq0NGOPS+f6z3x/3nTuliApjmqcM1lfTgePoz1FDG47ZofF58N8y96If62+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dnd-core": "^11.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-docgen": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.1.tgz",
|
||||
@@ -39072,9 +39086,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.2.tgz",
|
||||
"integrity": "sha512-5Ax5vbHxFgMBGGhQDm75Rrumm/HZC4ICFhMcJaM0UlqnC/4FKj/IaZtImZFupknyiiyUEcWHPQFA2kX3/VSv1A==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.3.tgz",
|
||||
"integrity": "sha512-oTfNXtS/K4PmASbcD+RyW7kxWGt3tknJL3RZodCMh+nnp3X4ng8vYg8yvIhTG/q0dqBxw7Ba8dHsfsEy4631vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -41753,9 +41767,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin/node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -42761,9 +42775,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz",
|
||||
"integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==",
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.5.tgz",
|
||||
"integrity": "sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -43235,21 +43249,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url/node_modules/@noble/hashes": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url/node_modules/webidl-conversions": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
@@ -44370,7 +44369,7 @@
|
||||
"cross-env": "^10.1.0",
|
||||
"fs-extra": "^11.3.5",
|
||||
"jest": "^30.4.2",
|
||||
"yeoman-test": "^11.5.2"
|
||||
"yeoman-test": "^11.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0",
|
||||
@@ -44436,8 +44435,8 @@
|
||||
"jed": "^1.1.1",
|
||||
"lodash": "^4.18.1",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*"
|
||||
}
|
||||
@@ -44463,9 +44462,9 @@
|
||||
"ace-builds": "^1.4.14",
|
||||
"brace": "^0.11.1",
|
||||
"memoize-one": "^5.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"packages/superset-ui-core": {
|
||||
@@ -44550,8 +44549,8 @@
|
||||
"@types/tinycolor2": "*",
|
||||
"antd": "^5.26.0",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*"
|
||||
}
|
||||
@@ -44715,7 +44714,7 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-calendar/node_modules/d3-array": {
|
||||
@@ -44776,7 +44775,7 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-country-map/node_modules/d3-array": {
|
||||
@@ -44804,7 +44803,7 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-horizon/node_modules/d3-array": {
|
||||
@@ -44832,7 +44831,7 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-parallel-coordinates": {
|
||||
@@ -44847,7 +44846,7 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-partition": {
|
||||
@@ -44865,8 +44864,8 @@
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-rose": {
|
||||
@@ -44883,7 +44882,7 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-world-map": {
|
||||
@@ -44901,7 +44900,7 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-world-map/node_modules/d3-array": {
|
||||
@@ -44988,7 +44987,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"dayjs": "^1.11.21",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-preset-chart-nvd3/node_modules/dompurify": {
|
||||
@@ -45025,8 +45024,8 @@
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-ag-grid-table/node_modules/d3-array": {
|
||||
@@ -45070,8 +45069,8 @@
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"ol": "^10.8.0",
|
||||
"polished": "*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-echarts": {
|
||||
@@ -45093,7 +45092,7 @@
|
||||
"dayjs": "^1.11.21",
|
||||
"echarts": "*",
|
||||
"memoize-one": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-echarts/node_modules/acorn": {
|
||||
@@ -45142,9 +45141,9 @@
|
||||
"dayjs": "^1.11.21",
|
||||
"handlebars": "^4.7.8",
|
||||
"lodash": "^4.18.1",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-handlebars/node_modules/just-handlebars-helpers": {
|
||||
@@ -45175,8 +45174,8 @@
|
||||
"@superset-ui/core": "*",
|
||||
"lodash": "^4.18.1",
|
||||
"prop-types": "*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-point-cluster-map": {
|
||||
@@ -45194,8 +45193,8 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-table": {
|
||||
@@ -45224,8 +45223,8 @@
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"match-sorter": "^8.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-table/node_modules/d3-array": {
|
||||
@@ -45264,7 +45263,7 @@
|
||||
"@superset-ui/core": "*",
|
||||
"@types/lodash": "*",
|
||||
"@types/react": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-word-cloud/node_modules/@types/d3-scale": {
|
||||
@@ -45324,8 +45323,8 @@
|
||||
"@superset-ui/core": "*",
|
||||
"dayjs": "^1.11.21",
|
||||
"mapbox-gl": ">=1.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"mapbox-gl": {
|
||||
|
||||
@@ -203,13 +203,13 @@
|
||||
"ol": "^10.9.0",
|
||||
"query-string": "9.4.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.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.2.0",
|
||||
"react-dom": "^18.3.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.2",
|
||||
"@storybook/addon-links": "10.4.2",
|
||||
"@storybook/react-webpack5": "10.4.2",
|
||||
"@storybook/addon-docs": "10.4.3",
|
||||
"@storybook/addon-links": "10.4.3",
|
||||
"@storybook/react-webpack5": "10.4.3",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.40",
|
||||
"@swc/core": "^1.15.41",
|
||||
"@swc/plugin-emotion": "^14.12.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
@@ -285,8 +285,8 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^25.9.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
@@ -346,16 +346,17 @@
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.69.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.8.3",
|
||||
"prettier": "3.8.4",
|
||||
"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.2",
|
||||
"storybook": "10.4.3",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
@@ -369,7 +370,7 @@
|
||||
"webpack": "^5.107.2",
|
||||
"webpack-bundle-analyzer": "^5.3.0",
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.4",
|
||||
"webpack-dev-server": "^5.2.5",
|
||||
"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.2"
|
||||
"yeoman-test": "^11.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 4.0.0",
|
||||
|
||||
@@ -18,14 +18,6 @@
|
||||
"types": "./lib/authentication/index.d.ts",
|
||||
"default": "./lib/authentication/index.js"
|
||||
},
|
||||
"./chat": {
|
||||
"types": "./lib/chat/index.d.ts",
|
||||
"default": "./lib/chat/index.js"
|
||||
},
|
||||
"./navigation": {
|
||||
"types": "./lib/navigation/index.d.ts",
|
||||
"default": "./lib/navigation/index.js"
|
||||
},
|
||||
"./commands": {
|
||||
"types": "./lib/commands/index.d.ts",
|
||||
"default": "./lib/commands/index.js"
|
||||
@@ -105,8 +97,8 @@
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*",
|
||||
"lodash": "^4.18.1",
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Chat contribution API for Superset extensions.
|
||||
*
|
||||
* Chat is a dedicated contribution type (not a view): an extension registers
|
||||
* a chat via {@link registerChat} and the host owns where and how it is
|
||||
* mounted. The host applies singleton resolution — multiple chat extensions
|
||||
* may register, but exactly one is active at a time.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { chat } from '@apache-superset/core';
|
||||
*
|
||||
* chat.registerChat(
|
||||
* { id: 'acme.chat', name: 'Acme Chat' },
|
||||
* () => <AcmeTrigger />,
|
||||
* () => <AcmePanel />,
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import type { Disposable, Event } from '../common';
|
||||
|
||||
export interface Chat {
|
||||
/** The unique identifier for the chat. */
|
||||
id: string;
|
||||
/** The display name of the chat. */
|
||||
name: string;
|
||||
/** Optional description of the chat, for display in contribution manifests. */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export type DisplayMode = 'floating' | 'panel';
|
||||
|
||||
/**
|
||||
* Registers a chat provider. The host applies singleton resolution — only one
|
||||
* chat is active at a time: the most recently registered chat wins, and
|
||||
* disposing it restores the previously registered one. Re-registering an id
|
||||
* replaces that registration in place.
|
||||
*
|
||||
* When a registration with a different id takes over the active slot (or the
|
||||
* active chat is disposed), the host closes the panel first, firing
|
||||
* {@link onDidClose}; an in-place same-id replacement keeps the open state.
|
||||
*
|
||||
* Disposing the returned Disposable unregisters the chat.
|
||||
*
|
||||
* @param chat The chat descriptor (id, name).
|
||||
* @param trigger A function returning the collapsed bubble element. Owned by
|
||||
* the extension — dynamic state such as unread counts and badges lives here.
|
||||
* Hidden by the host when in panel mode.
|
||||
* @param panel A function returning the chat panel element. Mounted by the
|
||||
* host as a floating overlay in 'floating' mode, or docked at the side of
|
||||
* the viewport in 'panel' mode (the reference host docks a fixed-width
|
||||
* overlay at the right edge; hosts may integrate a true layout slot
|
||||
* instead). Same component in both modes.
|
||||
* @returns A Disposable that unregisters the chat when disposed.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* chat.registerChat(
|
||||
* { id: 'acme.chat', name: 'Acme Chat' },
|
||||
* () => <AcmeTrigger />,
|
||||
* () => <AcmePanel />,
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export declare function registerChat(
|
||||
chat: Chat,
|
||||
trigger: () => ReactElement,
|
||||
panel: () => ReactElement,
|
||||
): Disposable;
|
||||
|
||||
/**
|
||||
* Returns the active chat descriptor.
|
||||
*
|
||||
* @returns A copy of the active Chat descriptor, or undefined if none is
|
||||
* registered. Mutating the returned object has no effect on the registry.
|
||||
*/
|
||||
export declare function getChat(): Chat | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when a chat is registered.
|
||||
*/
|
||||
export declare const onDidRegisterChat: Event<Chat>;
|
||||
|
||||
/**
|
||||
* Event fired when a chat is unregistered.
|
||||
*/
|
||||
export declare const onDidUnregisterChat: Event<Chat>;
|
||||
|
||||
/**
|
||||
* Opens the active chat's panel.
|
||||
*
|
||||
* Acts on whichever chat is active, regardless of which extension calls it.
|
||||
* No-op when no chat is registered or the panel is already open.
|
||||
*/
|
||||
export declare function open(): void;
|
||||
|
||||
/**
|
||||
* Closes the active chat's panel.
|
||||
*
|
||||
* Acts on whichever chat is active, regardless of which extension calls it.
|
||||
* No-op when the panel is not open.
|
||||
*/
|
||||
export declare function close(): void;
|
||||
|
||||
/**
|
||||
* Returns whether the active chat's panel is currently open.
|
||||
*
|
||||
* @returns True if the chat panel is open.
|
||||
*/
|
||||
export declare function isOpen(): boolean;
|
||||
|
||||
/**
|
||||
* Event fired when the chat panel opens. Also fired by the host's own
|
||||
* controls, not only by an extension's open() call.
|
||||
*/
|
||||
export declare const onDidOpen: Event<void>;
|
||||
|
||||
/**
|
||||
* Event fired when the chat panel closes. Also fired when the host closes the
|
||||
* panel itself, e.g. because the active chat was disposed or displaced by a
|
||||
* different chat.
|
||||
*/
|
||||
export declare const onDidClose: Event<void>;
|
||||
|
||||
/**
|
||||
* Returns the current display mode.
|
||||
*
|
||||
* @returns The current DisplayMode.
|
||||
*/
|
||||
export declare function getDisplayMode(): DisplayMode;
|
||||
|
||||
/**
|
||||
* Sets the display mode.
|
||||
*
|
||||
* The mode is host-global and applies to whichever chat is active, regardless
|
||||
* of which extension calls it. Hosts may also change the mode through their
|
||||
* own controls — use onDidChangeDisplayMode to observe all changes rather than
|
||||
* assuming the last setDisplayMode() call won.
|
||||
*
|
||||
* @param displayMode The display mode to switch to.
|
||||
*/
|
||||
export declare function setDisplayMode(displayMode: DisplayMode): void;
|
||||
|
||||
/**
|
||||
* Event fired when the display mode changes, whether triggered by an
|
||||
* extension via setDisplayMode() or by host-provided controls.
|
||||
*/
|
||||
export declare const onDidChangeDisplayMode: Event<DisplayMode>;
|
||||
|
||||
/**
|
||||
* Event fired when the panel is resized in panel mode.
|
||||
*
|
||||
* The host owns the resizer handle and drag interaction; a host without a
|
||||
* resizer never fires this event. (The reference host mounts the panel at a
|
||||
* fixed width and does not provide a resizer, so subscribers receive no
|
||||
* events there.) Listen to this event to adapt internal layout to the
|
||||
* available width; do not rely on it firing.
|
||||
*/
|
||||
export declare const onDidResizePanel: Event<{ width: number }>;
|
||||
|
||||
// TODO: client actions API — tool availability functions will be added here
|
||||
// once the client_actions SIP is finalized. The chat namespace is the
|
||||
// intended integration point between the two SIPs.
|
||||
@@ -223,6 +223,8 @@ export interface Extension {
|
||||
dependencies: string[];
|
||||
/** Human-readable description of the extension */
|
||||
description: string;
|
||||
/** List of other extensions that this extension depends on */
|
||||
extensionDependencies: string[];
|
||||
/** Unique identifier for the extension */
|
||||
id: string;
|
||||
/** Human-readable name of the extension */
|
||||
|
||||
@@ -23,10 +23,9 @@
|
||||
* This module defines the aggregate interfaces used by the extension.json
|
||||
* manifest and the `superset-extensions` build command. Individual metadata
|
||||
* types are defined in their respective namespace modules (commands, views,
|
||||
* menus, editors, chat) and re-exported here for the manifest schema.
|
||||
* menus, editors) and re-exported here for the manifest schema.
|
||||
*/
|
||||
|
||||
import { Chat } from '../chat';
|
||||
import { Command } from '../commands';
|
||||
import { View } from '../views';
|
||||
import { Menu } from '../menus';
|
||||
@@ -72,8 +71,7 @@ export interface MenuContributions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates all contributions (commands, menus, views, editors, and chat)
|
||||
* provided by an extension or module.
|
||||
* Aggregates all contributions (commands, menus, views, and editors) provided by an extension or module.
|
||||
*/
|
||||
export interface Contributions {
|
||||
/** List of commands. */
|
||||
@@ -84,10 +82,4 @@ export interface Contributions {
|
||||
views: ViewContributions;
|
||||
/** List of editors. */
|
||||
editors?: Editor[];
|
||||
/**
|
||||
* The chat contributed by the extension — at most one per extension, since
|
||||
* the host applies singleton resolution and renders exactly one active
|
||||
* chat at a time.
|
||||
*/
|
||||
chat?: Chat;
|
||||
}
|
||||
|
||||
@@ -18,12 +18,10 @@
|
||||
*/
|
||||
export * as common from './common';
|
||||
export * as authentication from './authentication';
|
||||
export * as chat from './chat';
|
||||
export * as commands from './commands';
|
||||
export * as editors from './editors';
|
||||
export * as extensions from './extensions';
|
||||
export * as menus from './menus';
|
||||
export * as navigation from './navigation';
|
||||
export * as sqlLab from './sqlLab';
|
||||
export * as views from './views';
|
||||
export * as contributions from './contributions';
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Navigation namespace for Superset extensions.
|
||||
*
|
||||
* Exposes the current application surface so extensions can react to route
|
||||
* changes without polling. Entity-level context (chart, dashboard, dataset)
|
||||
* is intentionally not included here — surface-specific namespaces that
|
||||
* resolve entity payloads are introduced in later phases.
|
||||
*/
|
||||
|
||||
import { Event } from '../common';
|
||||
|
||||
/**
|
||||
* The set of top-level application surfaces.
|
||||
*
|
||||
* `'explore'`, `'dashboard'` and `'dataset'` are the single-entity
|
||||
* editing/viewing surfaces. `'chart_list'`, `'dashboard_list'` and
|
||||
* `'dataset_list'` are the browse/list surfaces, distinct from those because no
|
||||
* single entity is active. `'sqllab'` is the SQL editor where
|
||||
* `sqlLab.getCurrentTab()` resolves; `'query_history'` and `'saved_queries'`
|
||||
* are the related SQL Lab browse pages, which are not the editor. `'home'` is
|
||||
* the welcome surface and the fallback for any route not explicitly enumerated.
|
||||
*/
|
||||
export type Page =
|
||||
| 'dashboard'
|
||||
| 'dashboard_list'
|
||||
| 'explore'
|
||||
| 'chart_list'
|
||||
| 'sqllab'
|
||||
| 'query_history'
|
||||
| 'saved_queries'
|
||||
| 'dataset'
|
||||
| 'dataset_list'
|
||||
| 'home';
|
||||
|
||||
/**
|
||||
* Returns the current page surface.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const page = navigation.getPage();
|
||||
* if (page === 'dashboard') {
|
||||
* // react to being on a dashboard surface
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export declare function getPage(): Page;
|
||||
|
||||
/**
|
||||
* Event fired whenever the user navigates to a different surface.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sub = navigation.onDidChangePage(page => {
|
||||
* if (page === 'dashboard') {
|
||||
* // react to navigating onto a dashboard surface
|
||||
* }
|
||||
* });
|
||||
* // later:
|
||||
* sub.dispose();
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidChangePage: Event<Page>;
|
||||
@@ -16,13 +16,26 @@
|
||||
* 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 } = require('@apache-superset/core/utils');
|
||||
const logging = loadLogging();
|
||||
|
||||
jest.spyOn(logging, 'debug').mockImplementation();
|
||||
jest.spyOn(logging, 'log').mockImplementation();
|
||||
@@ -50,20 +63,24 @@ test('should pipe to `console` methods', () => {
|
||||
});
|
||||
|
||||
test('should use noop functions when console unavailable', () => {
|
||||
const originalConsole = window.console;
|
||||
Object.assign(window, { console: undefined });
|
||||
const { logging } = require('@apache-superset/core/utils');
|
||||
try {
|
||||
const logging = loadLogging();
|
||||
|
||||
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 });
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
"ace-builds": "^1.4.14",
|
||||
"brace": "^0.11.1",
|
||||
"memoize-one": "^5.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -101,8 +101,8 @@
|
||||
"@types/tinycolor2": "*",
|
||||
"antd": "^5.26.0",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*"
|
||||
},
|
||||
|
||||
@@ -17,15 +17,23 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
import { Avatar as AntdAvatar } from 'antd';
|
||||
import type { AvatarProps, GroupProps as AvatarGroupProps } from './types';
|
||||
|
||||
export function Avatar(props: AvatarProps) {
|
||||
return <AntdAvatar {...props} />;
|
||||
}
|
||||
export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>((props, ref) => (
|
||||
<AntdAvatar ref={ref} {...props} />
|
||||
));
|
||||
|
||||
export function AvatarGroup(props: AvatarGroupProps) {
|
||||
return <AntdAvatar.Group {...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 type { AvatarProps, AvatarGroupProps };
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Children, ReactElement, Fragment } from 'react';
|
||||
import { Children, ReactElement, Fragment, forwardRef, Ref } 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' },
|
||||
};
|
||||
|
||||
export function Button(props: ButtonProps) {
|
||||
function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
|
||||
const {
|
||||
tooltip,
|
||||
placement,
|
||||
@@ -160,6 +160,7 @@ export function Button(props: ButtonProps) {
|
||||
|
||||
const button = (
|
||||
<AntdButton
|
||||
ref={ref as Ref<HTMLButtonElement & HTMLAnchorElement>}
|
||||
href={disabled ? undefined : href}
|
||||
disabled={disabled}
|
||||
type={antdType}
|
||||
@@ -235,4 +236,6 @@ export function Button(props: ButtonProps) {
|
||||
return button;
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLElement, ButtonProps>(ButtonInner);
|
||||
|
||||
export type { ButtonProps, OnClickHandler };
|
||||
|
||||
@@ -75,7 +75,10 @@ export const DropdownButton = ({
|
||||
id={`${kebabCase(tooltip)}-tooltip`}
|
||||
title={tooltip}
|
||||
>
|
||||
{button}
|
||||
{/* 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>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,7 +240,10 @@ export function EditableTitle({
|
||||
t("You don't have the rights to alter this title.")
|
||||
}
|
||||
>
|
||||
{titleComponent}
|
||||
{/* 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>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,47 +16,54 @@
|
||||
* 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 = ({
|
||||
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}
|
||||
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}`}
|
||||
>
|
||||
{iconTooltip}
|
||||
</Tooltip>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return iconTooltip;
|
||||
};
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
id="tooltip"
|
||||
title={tooltip}
|
||||
placement={placement}
|
||||
mouseEnterDelay={mouseEnterDelay}
|
||||
mouseLeaveDelay={mouseLeaveDelay}
|
||||
>
|
||||
{iconTooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return iconTooltip;
|
||||
},
|
||||
);
|
||||
|
||||
export type { IconTooltipProps };
|
||||
|
||||
@@ -165,7 +165,7 @@ import {
|
||||
SlackOutlined,
|
||||
ApiOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { FC } from 'react';
|
||||
import { ForwardRefExoticComponent, RefAttributes, forwardRef } from 'react';
|
||||
import { IconType } from './types';
|
||||
import { BaseIconComponent } from './BaseIcon';
|
||||
|
||||
@@ -323,19 +323,25 @@ type AntdIconNames = keyof typeof AntdIcons;
|
||||
|
||||
export const antdEnhancedIcons: Record<
|
||||
AntdIconNames,
|
||||
FC<IconType>
|
||||
ForwardRefExoticComponent<IconType & RefAttributes<HTMLSpanElement>>
|
||||
> = Object.keys(AntdIcons)
|
||||
.filter(key => !EXCLUDED_ICONS.some(excluded => key.includes(excluded)))
|
||||
.reduce(
|
||||
(acc, key) => {
|
||||
acc[key as AntdIconNames] = (props: IconType) => (
|
||||
<BaseIconComponent
|
||||
component={AntdIcons[key as AntdIconNames]}
|
||||
fileName={key}
|
||||
{...props}
|
||||
/>
|
||||
acc[key as AntdIconNames] = forwardRef<HTMLSpanElement, IconType>(
|
||||
(props, ref) => (
|
||||
<BaseIconComponent
|
||||
ref={ref}
|
||||
component={AntdIcons[key as AntdIconNames]}
|
||||
fileName={key}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<AntdIconNames, FC<IconType>>,
|
||||
{} as Record<
|
||||
AntdIconNames,
|
||||
ForwardRefExoticComponent<IconType & RefAttributes<HTMLSpanElement>>
|
||||
>,
|
||||
);
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { FC, SVGProps, useEffect, useRef, useState } from 'react';
|
||||
import { FC, SVGProps, forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import TransparentIcon from './svgs/transparent.svg';
|
||||
import { IconType } from './types';
|
||||
import { BaseIconComponent } from './BaseIcon';
|
||||
|
||||
const AsyncIcon = (props: IconType) => {
|
||||
const AsyncIcon = forwardRef<HTMLSpanElement, IconType>((props, ref) => {
|
||||
const [, setLoaded] = useState(false);
|
||||
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
|
||||
const { fileName, customIcons, iconSize, iconColor, viewBox, ...restProps } =
|
||||
@@ -46,6 +46,7 @@ const AsyncIcon = (props: IconType) => {
|
||||
|
||||
return (
|
||||
<BaseIconComponent
|
||||
ref={ref}
|
||||
component={ImportedSVG.current || TransparentIcon}
|
||||
fileName={fileName}
|
||||
customIcons={customIcons}
|
||||
@@ -55,6 +56,6 @@ const AsyncIcon = (props: IconType) => {
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default AsyncIcon;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* 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';
|
||||
|
||||
@@ -35,65 +36,78 @@ const genAriaLabel = (fileName: string) => {
|
||||
return name.toLowerCase();
|
||||
};
|
||||
|
||||
export const BaseIconComponent: React.FC<
|
||||
export const BaseIconComponent = forwardRef<
|
||||
HTMLSpanElement | SVGSVGElement,
|
||||
BaseIconProps & Omit<IconType, 'component'>
|
||||
> = ({
|
||||
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,
|
||||
};
|
||||
>(
|
||||
(
|
||||
{
|
||||
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,
|
||||
};
|
||||
|
||||
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'}
|
||||
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}
|
||||
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)}
|
||||
aria-label={ariaLabel}
|
||||
data-test={ariaLabel}
|
||||
{...(rest as AntdIconType)}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<Component
|
||||
role={whatRole}
|
||||
style={style}
|
||||
aria-label={ariaLabel}
|
||||
data-test={ariaLabel}
|
||||
{...(rest as AntdIconType)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -17,12 +17,16 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { FC } from 'react';
|
||||
import { ForwardRefExoticComponent, RefAttributes, forwardRef } 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
|
||||
@@ -58,15 +62,17 @@ const customIcons = [
|
||||
'Undo',
|
||||
] as const;
|
||||
|
||||
type CustomIconType = Record<(typeof customIcons)[number], FC<IconType>>;
|
||||
type CustomIconType = Record<(typeof customIcons)[number], IconComponent>;
|
||||
|
||||
const iconOverrides: CustomIconType = {} as CustomIconType;
|
||||
customIcons.forEach(customIcon => {
|
||||
const fileName = customIcon
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
||||
.toLowerCase();
|
||||
iconOverrides[customIcon] = (props: IconType) => (
|
||||
<AsyncIcon customIcons fileName={fileName} {...props} />
|
||||
iconOverrides[customIcon] = forwardRef<HTMLSpanElement, IconType>(
|
||||
(props, ref) => (
|
||||
<AsyncIcon ref={ref} customIcons fileName={fileName} {...props} />
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -74,7 +80,7 @@ export type IconNameType =
|
||||
| keyof typeof antdEnhancedIcons
|
||||
| keyof typeof iconOverrides;
|
||||
|
||||
type IconComponentType = Record<IconNameType, FC<IconType>>;
|
||||
type IconComponentType = Record<IconNameType, IconComponent>;
|
||||
|
||||
export const Icons: IconComponentType = {
|
||||
...antdEnhancedIcons,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* 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';
|
||||
@@ -23,7 +24,7 @@ import { DatasetTypeLabel } from './reusable/DatasetTypeLabel';
|
||||
import { PublishedLabel } from './reusable/PublishedLabel';
|
||||
import type { LabelProps } from './types';
|
||||
|
||||
export function Label(props: LabelProps) {
|
||||
export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
|
||||
const theme = useTheme();
|
||||
// Use Ant Design's motion duration instead of deprecated transitionTiming
|
||||
const {
|
||||
@@ -71,6 +72,7 @@ export function Label(props: LabelProps) {
|
||||
|
||||
return (
|
||||
<Tag
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
role={onClick ? 'button' : undefined}
|
||||
style={style}
|
||||
@@ -81,6 +83,6 @@ export function Label(props: LabelProps) {
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
});
|
||||
export { DatasetTypeLabel, PublishedLabel };
|
||||
export type { LabelType } from './types';
|
||||
|
||||
@@ -371,6 +371,9 @@ 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,11 +16,15 @@
|
||||
* 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 = (props: PopoverProps) => <AntdPopover {...props} />;
|
||||
export const Popover = forwardRef<TooltipRef, PopoverProps>((props, ref) => (
|
||||
<AntdPopover ref={ref} {...props} />
|
||||
));
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { MouseEventHandler, forwardRef } from 'react';
|
||||
import { MouseEventHandler } 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 {
|
||||
@@ -32,25 +31,19 @@ const RefreshLabel = ({
|
||||
onClick,
|
||||
tooltipContent,
|
||||
disabled,
|
||||
}: 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>
|
||||
);
|
||||
};
|
||||
}: 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>
|
||||
);
|
||||
|
||||
export default RefreshLabel;
|
||||
|
||||
@@ -16,17 +16,22 @@
|
||||
* 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 = ({ overlayStyle, ...props }: TooltipProps) => (
|
||||
<AntdTooltip
|
||||
styles={{
|
||||
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
root: overlayStyle ?? {},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
export const Tooltip = forwardRef<TooltipRef, TooltipProps>(
|
||||
({ overlayStyle, ...props }, ref) => (
|
||||
<AntdTooltip
|
||||
ref={ref}
|
||||
styles={{
|
||||
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
root: overlayStyle ?? {},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
export type { TooltipProps, TooltipPlacement };
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -36,6 +36,6 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -39,6 +39,6 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,6 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"dayjs": "^1.11.21",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"ol": "^10.8.0",
|
||||
"polished": "*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"dayjs": "^1.11.21",
|
||||
"echarts": "*",
|
||||
"memoize-one": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
"handlebars": "^4.7.8",
|
||||
"lodash": "^4.18.1",
|
||||
"dayjs": "^1.11.21",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.3.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 } from '@apache-superset/core/theme';
|
||||
import { useTheme, useThemeMode } 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,6 +37,7 @@ const HandlebarsTemplateControl = (
|
||||
props: CustomControlConfig<HandlebarsCustomControlProps>,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = useThemeMode();
|
||||
const val = String(
|
||||
props?.value ? props?.value : props?.default ? props?.default : '',
|
||||
);
|
||||
@@ -65,7 +66,7 @@ const HandlebarsTemplateControl = (
|
||||
</div>
|
||||
</ControlHeader>
|
||||
<CodeEditor
|
||||
theme="dark"
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
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 } from '@apache-superset/core/theme';
|
||||
import { useTheme, useThemeMode } 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,6 +35,7 @@ interface StyleCustomControlProps {
|
||||
|
||||
const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = useThemeMode();
|
||||
const htmlSanitization = props.htmlSanitization ?? true;
|
||||
|
||||
const defaultValue = props?.value
|
||||
@@ -63,7 +64,7 @@ const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
|
||||
</div>
|
||||
</ControlHeader>
|
||||
<CodeEditor
|
||||
theme="dark"
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
mode="css"
|
||||
value={props.value}
|
||||
defaultValue={defaultValue}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.29.7",
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"match-sorter": "^8.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@superset-ui/core": "*",
|
||||
"@types/lodash": "*",
|
||||
"@types/react": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.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.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"mapbox-gl": {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { AriaAttributes } from 'react';
|
||||
import { AriaAttributes, Ref } from 'react';
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
import jQuery from 'jquery';
|
||||
@@ -98,31 +98,39 @@ jest.mock('rehype-raw', () => () => jest.fn());
|
||||
// Tests should override this when needed
|
||||
jest.mock('@superset-ui/core/components/Icons/AsyncIcon', () => ({
|
||||
__esModule: true,
|
||||
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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
// 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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
),
|
||||
StyledIcon: ({
|
||||
component: Component,
|
||||
role,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { act } from 'react';
|
||||
import { QueryState } from '@superset-ui/core';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -189,15 +189,19 @@ const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
|
||||
placement="bottomLeft"
|
||||
trigger="click"
|
||||
>
|
||||
<DatabaseSelector
|
||||
key={`db-selector-${db ? db.id : 'no-db'}:${catalog ?? 'no-catalog'}:${
|
||||
schema ?? 'no-schema'
|
||||
}`}
|
||||
{...dbSelectorProps}
|
||||
emptyState={<EmptyState />}
|
||||
sqlLabMode
|
||||
onOpenModal={openSelectorModal}
|
||||
/>
|
||||
{/* 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>
|
||||
</Popover>
|
||||
<StyledDivider />
|
||||
<TableExploreTree queryEditorId={activeQEId} />
|
||||
|
||||
@@ -1,464 +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 { 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();
|
||||
});
|
||||
@@ -1,420 +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 { 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;
|
||||
@@ -1,287 +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 { act, render, screen } from 'spec/helpers/testing-library';
|
||||
import { chat } from 'src/core/chat';
|
||||
import ChatMount from '.';
|
||||
|
||||
const disposables: Array<{ dispose: () => void }> = [];
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
disposables.forEach(d => d.dispose());
|
||||
disposables.length = 0;
|
||||
// Reset host-owned state shared across tests in this module.
|
||||
chat.close();
|
||||
chat.setDisplayMode('floating');
|
||||
});
|
||||
});
|
||||
|
||||
test('renders nothing when no chat extension is registered', () => {
|
||||
render(<ChatMount />);
|
||||
|
||||
expect(screen.queryByTestId('chat-mount')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the trigger bubble of the registered chat', () => {
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme Chat' },
|
||||
() => <button type="button">Acme Bubble</button>,
|
||||
() => <div>Acme Panel</div>,
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChatMount />);
|
||||
|
||||
expect(screen.getByTestId('chat-mount')).toBeInTheDocument();
|
||||
expect(screen.getByText('Acme Bubble')).toBeInTheDocument();
|
||||
// The panel stays unmounted until the chat is opened.
|
||||
expect(screen.queryByText('Acme Panel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('mounts the panel when the chat opens and unmounts it on close', () => {
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme Chat' },
|
||||
() => <button type="button">Acme Bubble</button>,
|
||||
() => <div>Acme Panel</div>,
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChatMount />);
|
||||
|
||||
act(() => chat.open());
|
||||
|
||||
expect(screen.getByText('Acme Panel')).toBeInTheDocument();
|
||||
// In floating mode the trigger stays mounted alongside the open panel.
|
||||
expect(screen.getByText('Acme Bubble')).toBeInTheDocument();
|
||||
|
||||
act(() => chat.close());
|
||||
|
||||
expect(screen.queryByText('Acme Panel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the last-registered chat when several are installed', () => {
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'first.chat', name: 'First Chat' },
|
||||
() => <div>First Bubble</div>,
|
||||
() => <div>First Panel</div>,
|
||||
),
|
||||
chat.registerChat(
|
||||
{ id: 'second.chat', name: 'Second Chat' },
|
||||
() => <div>Second Bubble</div>,
|
||||
() => <div>Second Panel</div>,
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChatMount />);
|
||||
|
||||
// Last-loaded wins: the second registration takes over the singleton slot.
|
||||
expect(screen.getByText('Second Bubble')).toBeInTheDocument();
|
||||
expect(screen.queryByText('First Bubble')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('reacts to a chat registering after the initial render', () => {
|
||||
render(<ChatMount />);
|
||||
|
||||
expect(screen.queryByTestId('chat-mount')).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme Chat' },
|
||||
() => <button type="button">Acme Bubble</button>,
|
||||
() => <div>Acme Panel</div>,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Acme Bubble')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('a takeover mounts the incoming chat closed', () => {
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'first.chat', name: 'First Chat' },
|
||||
() => <div>First Bubble</div>,
|
||||
() => <div>First Panel</div>,
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChatMount />);
|
||||
act(() => chat.open());
|
||||
expect(screen.getByText('First Panel')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'second.chat', name: 'Second Chat' },
|
||||
() => <div>Second Bubble</div>,
|
||||
() => <div>Second Panel</div>,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// The displaced chat's open state must not leak into the winner.
|
||||
expect(screen.getByText('Second Bubble')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Second Panel')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('First Panel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('panel mode docks the open panel and hides the trigger', () => {
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme Chat' },
|
||||
() => <button type="button">Acme Bubble</button>,
|
||||
() => <div>Acme Panel</div>,
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChatMount />);
|
||||
|
||||
act(() => {
|
||||
chat.setDisplayMode('panel');
|
||||
chat.open();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Acme Panel')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Acme Bubble')).not.toBeInTheDocument();
|
||||
|
||||
act(() => chat.close());
|
||||
|
||||
// A closed chat in panel mode renders nothing — the trigger is hidden too.
|
||||
expect(screen.queryByTestId('chat-mount')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('a crashing panel does not take the trigger down with it', () => {
|
||||
const FailingPanel = () => {
|
||||
throw new Error('panel blew up');
|
||||
};
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme Chat' },
|
||||
() => <button type="button">Acme Bubble</button>,
|
||||
() => <FailingPanel />,
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChatMount />);
|
||||
act(() => chat.open());
|
||||
|
||||
// The panel's boundary contains the crash; the trigger keeps rendering so
|
||||
// the user is not stranded without a way back.
|
||||
expect(screen.queryByText('panel blew up')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Acme Bubble')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('isolates a failing trigger so it does not crash the host', () => {
|
||||
const FailingTrigger = () => {
|
||||
throw new Error('chat blew up');
|
||||
};
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme Chat' },
|
||||
() => <FailingTrigger />,
|
||||
() => <div>Acme Panel</div>,
|
||||
),
|
||||
);
|
||||
|
||||
// The host-owned error boundary catches the failure; render does not throw.
|
||||
expect(() => render(<ChatMount />)).not.toThrow();
|
||||
// The mount slot still renders (the boundary lives inside it), confirming
|
||||
// the provider was actually exercised and contained.
|
||||
expect(screen.getByTestId('chat-mount')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('isolates a chat whose provider function itself throws', () => {
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme Chat' },
|
||||
() => {
|
||||
throw new Error('provider blew up');
|
||||
},
|
||||
() => <div>Acme Panel</div>,
|
||||
),
|
||||
);
|
||||
|
||||
// ChatRenderer wraps provider() in a component so ErrorBoundary catches
|
||||
// synchronous throws from the provider function, not just from its output.
|
||||
expect(() => render(<ChatMount />)).not.toThrow();
|
||||
expect(screen.getByTestId('chat-mount')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('recovers from a crashed chat when a different chat takes over', () => {
|
||||
const FailingTrigger = () => {
|
||||
throw new Error('first chat blew up');
|
||||
};
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'first.chat', name: 'First Chat' },
|
||||
() => <FailingTrigger />,
|
||||
() => <div>First Panel</div>,
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChatMount />);
|
||||
expect(screen.queryByText('Second Bubble')).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'second.chat', name: 'Second Chat' },
|
||||
() => <div>Second Bubble</div>,
|
||||
() => <div>Second Panel</div>,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// The boundary is keyed per registration, so the latched crash from the
|
||||
// first chat does not blank the second one.
|
||||
expect(screen.getByText('Second Bubble')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('recovers when a crashed chat re-registers a fixed version under the same id', () => {
|
||||
const FailingTrigger = () => {
|
||||
throw new Error('broken release');
|
||||
};
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme Chat' },
|
||||
() => <FailingTrigger />,
|
||||
() => <div>Acme Panel</div>,
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChatMount />);
|
||||
expect(screen.queryByText('Fixed Bubble')).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
disposables.push(
|
||||
chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme Chat' },
|
||||
() => <div>Fixed Bubble</div>,
|
||||
() => <div>Acme Panel</div>,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Same id, new registrationId: the remounted boundary renders the fix.
|
||||
expect(screen.getByText('Fixed Bubble')).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { type ReactElement, useRef, useSyncExternalStore } from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { logging } from '@apache-superset/core/utils';
|
||||
import { css, useTheme } from '@apache-superset/core/theme';
|
||||
import { ErrorBoundary } from 'src/components/ErrorBoundary';
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
import { store } from 'src/views/store';
|
||||
import { getChatSnapshot, subscribeToChatState } from 'src/core/chat';
|
||||
|
||||
const CHAT_EDGE_MARGIN = 24;
|
||||
const PANEL_MODE_WIDTH = 400;
|
||||
|
||||
/**
|
||||
* Wraps a chat provider in a React component so that ErrorBoundary can catch
|
||||
* synchronous throws from the provider function itself. Calling `provider()`
|
||||
* inline (e.g. `{activeChat.panel()}`) would throw outside React's render
|
||||
* boundary and crash the host.
|
||||
*/
|
||||
const ChatRenderer = ({ provider }: { provider: () => ReactElement }) =>
|
||||
provider();
|
||||
|
||||
const ChatMount = () => {
|
||||
const theme = useTheme();
|
||||
// Notify at most once per registration; a crash can re-render and would
|
||||
// otherwise re-toast, while a replacement (new registrationId) deserves a
|
||||
// fresh notification if it crashes too.
|
||||
const crashNotifiedFor = useRef<number | null>(null);
|
||||
|
||||
// The active chat, the open state, and the display mode are read from one
|
||||
// immutable registry snapshot so a render never mixes state from two
|
||||
// different store versions (the tearing useSyncExternalStore prevents).
|
||||
const {
|
||||
open: panelOpen,
|
||||
mode,
|
||||
active,
|
||||
} = useSyncExternalStore(subscribeToChatState, getChatSnapshot);
|
||||
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { registrationId } = active;
|
||||
|
||||
const onProviderError = (error: Error) => {
|
||||
// Fault isolation: contain the crash, log it, surface a one-time
|
||||
// notification, and leave the slot empty rather than parking a
|
||||
// persistent error card.
|
||||
logging.error('[chat] provider crashed', error);
|
||||
if (crashNotifiedFor.current !== registrationId) {
|
||||
crashNotifiedFor.current = registrationId;
|
||||
store.dispatch(addDangerToast(t('The chat failed to load.')));
|
||||
}
|
||||
};
|
||||
|
||||
if (mode === 'panel') {
|
||||
// Panel mode hides the trigger and docks the panel to the right edge.
|
||||
// Interim approximation of the "layout slot between header and footer"
|
||||
// from the chat API contract — the dock overlays the page until the host
|
||||
// grows a real layout slot and resizer chrome.
|
||||
if (!panelOpen) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-test="chat-mount"
|
||||
css={css`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: ${PANEL_MODE_WIDTH}px;
|
||||
background: ${theme.colorBgContainer};
|
||||
box-shadow: ${theme.boxShadow};
|
||||
/* Above dashboard content and the toast layer, below modal dialogs. */
|
||||
z-index: ${theme.zIndexPopupBase + 2};
|
||||
`}
|
||||
>
|
||||
<ErrorBoundary
|
||||
key={registrationId}
|
||||
showMessage={false}
|
||||
onError={onProviderError}
|
||||
>
|
||||
<ChatRenderer provider={active.panel} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test="chat-mount"
|
||||
css={css`
|
||||
position: fixed;
|
||||
right: ${CHAT_EDGE_MARGIN}px;
|
||||
bottom: ${CHAT_EDGE_MARGIN}px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: ${theme.sizeUnit * 2}px;
|
||||
/* Above dashboard content and the toast layer, below modal dialogs. */
|
||||
z-index: ${theme.zIndexPopupBase + 2};
|
||||
`}
|
||||
>
|
||||
{/*
|
||||
Each provider gets its own boundary so a crashing panel cannot take
|
||||
the trigger down with it (the trigger is the user's only way back).
|
||||
Keyed by registrationId: Superset's ErrorBoundary latches its error
|
||||
state, so a takeover, fallback, or same-id re-registration must
|
||||
remount the boundary to recover.
|
||||
*/}
|
||||
{panelOpen && (
|
||||
<ErrorBoundary
|
||||
key={`panel-${registrationId}`}
|
||||
showMessage={false}
|
||||
onError={onProviderError}
|
||||
>
|
||||
<ChatRenderer provider={active.panel} />
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
<ErrorBoundary
|
||||
key={`trigger-${registrationId}`}
|
||||
showMessage={false}
|
||||
onError={onProviderError}
|
||||
>
|
||||
<ChatRenderer provider={active.trigger} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatMount;
|
||||
@@ -98,7 +98,10 @@ class CopyToClip extends Component<CopyToClipboardProps> {
|
||||
trigger={['hover']}
|
||||
arrow={{ pointAtCenter: true }}
|
||||
>
|
||||
{this.getDecoratedCopyNode()}
|
||||
{/* 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>
|
||||
</Tooltip>
|
||||
) : (
|
||||
this.getDecoratedCopyNode()
|
||||
|
||||
@@ -17,21 +17,25 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import { PropsWithoutRef, RefAttributes } from 'react';
|
||||
import { forwardRef, PropsWithoutRef, Ref, RefAttributes } from 'react';
|
||||
import { Link, LinkProps } from 'react-router-dom';
|
||||
import { isUrlExternal, parseUrl } from 'src/utils/urlUtils';
|
||||
|
||||
export const GenericLink = <S,>({
|
||||
to,
|
||||
component,
|
||||
replace,
|
||||
innerRef,
|
||||
children,
|
||||
...rest
|
||||
}: PropsWithoutRef<LinkProps<S>> & RefAttributes<HTMLAnchorElement>) => {
|
||||
type GenericLinkProps<S> = PropsWithoutRef<LinkProps<S>> &
|
||||
RefAttributes<HTMLAnchorElement>;
|
||||
|
||||
const GenericLinkInner = <S,>(
|
||||
{ to, component, replace, innerRef, children, ...rest }: GenericLinkProps<S>,
|
||||
ref: Ref<HTMLAnchorElement>,
|
||||
) => {
|
||||
if (typeof to === 'string' && isUrlExternal(to)) {
|
||||
return (
|
||||
<a data-test="external-link" href={sanitizeUrl(parseUrl(to))} {...rest}>
|
||||
<a
|
||||
ref={ref}
|
||||
data-test="external-link"
|
||||
href={sanitizeUrl(parseUrl(to))}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
@@ -42,10 +46,14 @@ export const GenericLink = <S,>({
|
||||
to={to}
|
||||
component={component}
|
||||
replace={replace}
|
||||
innerRef={innerRef}
|
||||
innerRef={innerRef ?? ref}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const GenericLink = forwardRef(GenericLinkInner) as <S>(
|
||||
props: GenericLinkProps<S> & { ref?: Ref<HTMLAnchorElement> },
|
||||
) => ReturnType<typeof GenericLinkInner>;
|
||||
|
||||
@@ -1,327 +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 { createElement } from 'react';
|
||||
import { chat, getActiveChat, getChatSnapshot } from './index';
|
||||
|
||||
const disposables: Array<{ dispose: () => void }> = [];
|
||||
|
||||
const trigger = () => createElement('button', null, 'Bubble');
|
||||
const panel = () => createElement('div', null, 'Panel');
|
||||
|
||||
afterEach(() => {
|
||||
disposables.forEach(d => d.dispose());
|
||||
disposables.length = 0;
|
||||
// Reset host-owned state shared across tests in this module.
|
||||
chat.close();
|
||||
chat.setDisplayMode('floating');
|
||||
});
|
||||
|
||||
test('getChat returns undefined when no chat is registered', () => {
|
||||
expect(chat.getChat()).toBeUndefined();
|
||||
expect(getActiveChat()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('registerChat resolves the registered chat with its providers', () => {
|
||||
const descriptor = { id: 'acme.chat', name: 'Acme Chat' };
|
||||
disposables.push(chat.registerChat(descriptor, trigger, panel));
|
||||
|
||||
expect(chat.getChat()).toEqual(descriptor);
|
||||
expect(getActiveChat()).toMatchObject({ chat: descriptor, trigger, panel });
|
||||
});
|
||||
|
||||
test('getChat returns a copy that cannot mutate the registry', () => {
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'acme.chat', name: 'Acme Chat' }, trigger, panel),
|
||||
);
|
||||
|
||||
const copy = chat.getChat();
|
||||
copy!.name = 'Hijacked';
|
||||
|
||||
expect(chat.getChat()?.name).toBe('Acme Chat');
|
||||
});
|
||||
|
||||
test('the last-registered chat wins when multiple are installed', () => {
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'first.chat', name: 'First' }, trigger, panel),
|
||||
chat.registerChat({ id: 'second.chat', name: 'Second' }, trigger, panel),
|
||||
);
|
||||
|
||||
expect(chat.getChat()?.id).toBe('second.chat');
|
||||
});
|
||||
|
||||
test('disposing the active chat falls back to the previous registration', () => {
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'first.chat', name: 'First' }, trigger, panel),
|
||||
);
|
||||
const second = chat.registerChat(
|
||||
{ id: 'second.chat', name: 'Second' },
|
||||
trigger,
|
||||
panel,
|
||||
);
|
||||
|
||||
expect(chat.getChat()?.id).toBe('second.chat');
|
||||
|
||||
second.dispose();
|
||||
|
||||
expect(chat.getChat()?.id).toBe('first.chat');
|
||||
});
|
||||
|
||||
test('re-registering an id replaces the previous registration', () => {
|
||||
const stalePanel = () => createElement('div', null, 'Stale');
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'acme.chat', name: 'Acme' }, trigger, stalePanel),
|
||||
chat.registerChat({ id: 'acme.chat', name: 'Acme v2' }, trigger, panel),
|
||||
);
|
||||
|
||||
expect(chat.getChat()?.name).toBe('Acme v2');
|
||||
expect(getActiveChat()?.panel).toBe(panel);
|
||||
});
|
||||
|
||||
test('each registration gets a distinct registrationId, including same-id replacements', () => {
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'acme.chat', name: 'Acme' }, trigger, panel),
|
||||
);
|
||||
const first = getActiveChat()?.registrationId;
|
||||
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'acme.chat', name: 'Acme v2' }, trigger, panel),
|
||||
);
|
||||
const second = getActiveChat()?.registrationId;
|
||||
|
||||
expect(first).toBeDefined();
|
||||
expect(second).toBeDefined();
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
test('disposing a registration twice unregisters only once', () => {
|
||||
const unregistered = jest.fn();
|
||||
disposables.push(chat.onDidUnregisterChat(unregistered));
|
||||
|
||||
const registration = chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme' },
|
||||
trigger,
|
||||
panel,
|
||||
);
|
||||
registration.dispose();
|
||||
registration.dispose();
|
||||
|
||||
expect(unregistered).toHaveBeenCalledTimes(1);
|
||||
expect(chat.getChat()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('onDidRegisterChat and onDidUnregisterChat fire with the descriptor', () => {
|
||||
const registered = jest.fn();
|
||||
const unregistered = jest.fn();
|
||||
disposables.push(
|
||||
chat.onDidRegisterChat(registered),
|
||||
chat.onDidUnregisterChat(unregistered),
|
||||
);
|
||||
|
||||
const descriptor = { id: 'acme.chat', name: 'Acme' };
|
||||
const registration = chat.registerChat(descriptor, trigger, panel);
|
||||
|
||||
expect(registered).toHaveBeenCalledWith(descriptor);
|
||||
expect(unregistered).not.toHaveBeenCalled();
|
||||
|
||||
registration.dispose();
|
||||
|
||||
expect(unregistered).toHaveBeenCalledWith(descriptor);
|
||||
});
|
||||
|
||||
test('a disposed event subscription stops receiving notifications', () => {
|
||||
const registered = jest.fn();
|
||||
const subscription = chat.onDidRegisterChat(registered);
|
||||
subscription.dispose();
|
||||
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'acme.chat', name: 'Acme' }, trigger, panel),
|
||||
);
|
||||
|
||||
expect(registered).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('open and close toggle the panel and fire once', () => {
|
||||
const opened = jest.fn();
|
||||
const closed = jest.fn();
|
||||
disposables.push(chat.onDidOpen(opened), chat.onDidClose(closed));
|
||||
|
||||
const descriptor = { id: 'acme.chat', name: 'Acme' };
|
||||
disposables.push(chat.registerChat(descriptor, trigger, panel));
|
||||
|
||||
expect(chat.isOpen()).toBe(false);
|
||||
|
||||
chat.open();
|
||||
// Opening an already-open panel is a no-op and must not re-fire.
|
||||
chat.open();
|
||||
|
||||
expect(chat.isOpen()).toBe(true);
|
||||
expect(opened).toHaveBeenCalledTimes(1);
|
||||
|
||||
chat.close();
|
||||
chat.close();
|
||||
|
||||
expect(chat.isOpen()).toBe(false);
|
||||
expect(closed).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('open is a no-op while no chat is registered', () => {
|
||||
const opened = jest.fn();
|
||||
disposables.push(chat.onDidOpen(opened));
|
||||
|
||||
chat.open();
|
||||
|
||||
expect(chat.isOpen()).toBe(false);
|
||||
expect(opened).not.toHaveBeenCalled();
|
||||
|
||||
// A registration arriving later therefore starts closed.
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'acme.chat', name: 'Acme' }, trigger, panel),
|
||||
);
|
||||
expect(chat.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
test('a takeover by a different id closes the displaced chat panel', () => {
|
||||
const closed = jest.fn();
|
||||
disposables.push(chat.onDidClose(closed));
|
||||
|
||||
const first = { id: 'first.chat', name: 'First' };
|
||||
disposables.push(chat.registerChat(first, trigger, panel));
|
||||
chat.open();
|
||||
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'second.chat', name: 'Second' }, trigger, panel),
|
||||
);
|
||||
|
||||
// The incoming chat must not mount into an open state it never requested.
|
||||
expect(chat.isOpen()).toBe(false);
|
||||
expect(closed).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('a same-id replacement keeps the open state', () => {
|
||||
const closed = jest.fn();
|
||||
disposables.push(chat.onDidClose(closed));
|
||||
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'acme.chat', name: 'Acme' }, trigger, panel),
|
||||
);
|
||||
chat.open();
|
||||
|
||||
// Upgrade in place: same id, new providers.
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'acme.chat', name: 'Acme v2' }, trigger, panel),
|
||||
);
|
||||
|
||||
expect(chat.isOpen()).toBe(true);
|
||||
expect(closed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('disposing the active chat while open closes it; the fallback starts closed', () => {
|
||||
const closed = jest.fn();
|
||||
disposables.push(chat.onDidClose(closed));
|
||||
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'first.chat', name: 'First' }, trigger, panel),
|
||||
);
|
||||
const second = { id: 'second.chat', name: 'Second' };
|
||||
const registration = chat.registerChat(second, trigger, panel);
|
||||
chat.open();
|
||||
|
||||
registration.dispose();
|
||||
|
||||
expect(chat.getChat()?.id).toBe('first.chat');
|
||||
expect(chat.isOpen()).toBe(false);
|
||||
expect(closed).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('disposing an inactive registration leaves the open state untouched', () => {
|
||||
const closed = jest.fn();
|
||||
disposables.push(chat.onDidClose(closed));
|
||||
|
||||
const inactive = chat.registerChat(
|
||||
{ id: 'first.chat', name: 'First' },
|
||||
trigger,
|
||||
panel,
|
||||
);
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'second.chat', name: 'Second' }, trigger, panel),
|
||||
);
|
||||
chat.open();
|
||||
|
||||
inactive.dispose();
|
||||
|
||||
expect(chat.isOpen()).toBe(true);
|
||||
expect(closed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('disposing the last chat while open resets the open state', () => {
|
||||
const registration = chat.registerChat(
|
||||
{ id: 'acme.chat', name: 'Acme' },
|
||||
trigger,
|
||||
panel,
|
||||
);
|
||||
chat.open();
|
||||
expect(chat.isOpen()).toBe(true);
|
||||
|
||||
registration.dispose();
|
||||
|
||||
expect(chat.isOpen()).toBe(false);
|
||||
|
||||
// A registration arriving much later must not inherit a stale open state.
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'late.chat', name: 'Late' }, trigger, panel),
|
||||
);
|
||||
expect(chat.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
test('mode defaults to floating and setDisplayMode fires only on change', () => {
|
||||
const modeChanged = jest.fn();
|
||||
disposables.push(chat.onDidChangeDisplayMode(modeChanged));
|
||||
|
||||
expect(chat.getDisplayMode()).toBe('floating');
|
||||
|
||||
// Setting the current mode is a no-op.
|
||||
chat.setDisplayMode('floating');
|
||||
expect(modeChanged).not.toHaveBeenCalled();
|
||||
|
||||
chat.setDisplayMode('panel');
|
||||
expect(chat.getDisplayMode()).toBe('panel');
|
||||
expect(modeChanged).toHaveBeenCalledWith('panel');
|
||||
});
|
||||
|
||||
test('the snapshot is immutable per version and consistent with the registry', () => {
|
||||
const before = getChatSnapshot();
|
||||
|
||||
disposables.push(
|
||||
chat.registerChat({ id: 'acme.chat', name: 'Acme' }, trigger, panel),
|
||||
);
|
||||
chat.open();
|
||||
|
||||
const after = getChatSnapshot();
|
||||
// Unchanged references for old snapshots; a new object per change.
|
||||
expect(after).not.toBe(before);
|
||||
expect(before.active).toBeUndefined();
|
||||
expect(after).toMatchObject({
|
||||
open: true,
|
||||
mode: 'floating',
|
||||
active: getActiveChat(),
|
||||
});
|
||||
expect(after.version).toBeGreaterThan(before.version);
|
||||
// Stable reference between changes.
|
||||
expect(getChatSnapshot()).toBe(after);
|
||||
});
|
||||
@@ -1,240 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Host implementation of the `chat` contribution type.
|
||||
*
|
||||
* Chat is a dedicated contribution type, not a view: extensions register via
|
||||
* the public `chat.registerChat()` and the host owns mounting, open/close
|
||||
* state, and the display mode. Multiple chat extensions may register, but the
|
||||
* host applies singleton resolution — the most-recently-registered chat is
|
||||
* active; disposing it falls back to the previous one.
|
||||
*
|
||||
* Open-state policy across active-chat transitions: when the active chat's
|
||||
* identity changes — a takeover by a different id, disposal falling back to a
|
||||
* different id, or disposal of the last chat — the panel is closed (firing
|
||||
* `onDidClose`) so the incoming chat never mounts into an open state it did
|
||||
* not request. A same-id re-registration is an upgrade in place and keeps the
|
||||
* open state.
|
||||
*
|
||||
* The public namespace (`chat`) is exposed to extensions on
|
||||
* `window.superset`; the other exports are host-internal accessors for
|
||||
* ChatMount and are NOT part of the public `@apache-superset/core` API.
|
||||
*/
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import type { chat as chatApi } from '@apache-superset/core';
|
||||
import { Disposable } from '../models';
|
||||
import { createValueEventEmitter, createEventEmitter } from '../utils';
|
||||
|
||||
type Chat = chatApi.Chat;
|
||||
type DisplayMode = chatApi.DisplayMode;
|
||||
|
||||
/** A registered chat: its descriptor plus the host-mountable providers. */
|
||||
export interface RegisteredChat {
|
||||
/** The chat descriptor passed to `registerChat`. */
|
||||
chat: Chat;
|
||||
/** Renders the collapsed bubble. Hidden by the host in panel mode. */
|
||||
trigger: () => ReactElement;
|
||||
/** Renders the chat panel, mounted per the current {@link DisplayMode}. */
|
||||
panel: () => ReactElement;
|
||||
/**
|
||||
* Unique per registration (a same-id re-registration gets a new one). The
|
||||
* host UI keys mounts and fault containment on it, so a replacement resets
|
||||
* crashed error boundaries instead of inheriting their latched state.
|
||||
*/
|
||||
registrationId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable snapshot of the whole chat state, rebuilt on every change.
|
||||
* Returned by reference from `getChatSnapshot` so `useSyncExternalStore`
|
||||
* consumers read registrations, open state, and mode from one consistent
|
||||
* object instead of tearing across separate live reads.
|
||||
*/
|
||||
export interface ChatSnapshot {
|
||||
/** Monotonic change counter, useful as a memo/effect dependency. */
|
||||
version: number;
|
||||
/** Whether the active chat's panel is open. */
|
||||
open: boolean;
|
||||
/** The current display mode. */
|
||||
mode: DisplayMode;
|
||||
/** The active registration, or undefined when none is registered. */
|
||||
active: RegisteredChat | undefined;
|
||||
}
|
||||
|
||||
/** Registration order is the singleton-resolution order: last entry wins. */
|
||||
const registrations: RegisteredChat[] = [];
|
||||
|
||||
let panelOpen = false;
|
||||
let nextRegistrationId = 1;
|
||||
|
||||
const registerEmitter = createEventEmitter<Chat>();
|
||||
const unregisterEmitter = createEventEmitter<Chat>();
|
||||
const openEmitter = createEventEmitter<void>();
|
||||
const closeEmitter = createEventEmitter<void>();
|
||||
const resizePanelEmitter = createEventEmitter<{ width: number }>();
|
||||
const modeEmitter = createValueEventEmitter<DisplayMode>('floating');
|
||||
|
||||
/**
|
||||
* Host-internal: resolves the active chat with its providers.
|
||||
* The most-recently-registered chat wins; when it is disposed the previous
|
||||
* registration takes over the slot again.
|
||||
*/
|
||||
export const getActiveChat = (): RegisteredChat | undefined =>
|
||||
registrations[registrations.length - 1];
|
||||
|
||||
let snapshot: ChatSnapshot = {
|
||||
version: 0,
|
||||
open: false,
|
||||
mode: modeEmitter.getCurrent(),
|
||||
active: undefined,
|
||||
};
|
||||
|
||||
const stateSubscribers = new Set<() => void>();
|
||||
|
||||
const notifyState = () => {
|
||||
snapshot = {
|
||||
version: snapshot.version + 1,
|
||||
open: panelOpen,
|
||||
mode: modeEmitter.getCurrent(),
|
||||
active: getActiveChat(),
|
||||
};
|
||||
stateSubscribers.forEach(fn => fn());
|
||||
};
|
||||
|
||||
export const subscribeToChatState = (listener: () => void): (() => void) => {
|
||||
stateSubscribers.add(listener);
|
||||
return () => {
|
||||
stateSubscribers.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
export const getChatSnapshot = (): ChatSnapshot => snapshot;
|
||||
|
||||
/** Closes the panel and fires `onDidClose`. */
|
||||
const closePanel = () => {
|
||||
panelOpen = false;
|
||||
closeEmitter.fire();
|
||||
};
|
||||
|
||||
const registerChat: typeof chatApi.registerChat = (
|
||||
chat: Chat,
|
||||
trigger: () => ReactElement,
|
||||
panel: () => ReactElement,
|
||||
): Disposable => {
|
||||
const previousActive = getActiveChat();
|
||||
|
||||
// Re-registering an id replaces the previous entry and moves it to the
|
||||
// most-recent position, mirroring the view registry's same-id semantics.
|
||||
const existingIndex = registrations.findIndex(r => r.chat.id === chat.id);
|
||||
if (existingIndex !== -1) {
|
||||
registrations.splice(existingIndex, 1);
|
||||
}
|
||||
|
||||
const entry: RegisteredChat = {
|
||||
chat,
|
||||
trigger,
|
||||
panel,
|
||||
registrationId: nextRegistrationId,
|
||||
};
|
||||
nextRegistrationId += 1;
|
||||
registrations.push(entry);
|
||||
registerEmitter.fire(chat);
|
||||
|
||||
// A takeover by a different id closes the displaced chat's panel so the
|
||||
// incoming chat never mounts already-open; a same-id replacement is an
|
||||
// upgrade in place and keeps the open state.
|
||||
if (panelOpen && previousActive && previousActive.chat.id !== chat.id) {
|
||||
closePanel();
|
||||
}
|
||||
notifyState();
|
||||
|
||||
return new Disposable(() => {
|
||||
const index = registrations.indexOf(entry);
|
||||
if (index === -1) {
|
||||
// Already removed — replaced by a same-id registration or disposed twice.
|
||||
return;
|
||||
}
|
||||
const wasActive = getActiveChat() === entry;
|
||||
registrations.splice(index, 1);
|
||||
unregisterEmitter.fire(chat);
|
||||
// Disposing the active chat closes its panel; the fallback chat (if any)
|
||||
// starts closed. Disposing an inactive registration leaves the open
|
||||
// state of the active chat untouched.
|
||||
if (panelOpen && wasActive) {
|
||||
closePanel();
|
||||
}
|
||||
notifyState();
|
||||
});
|
||||
};
|
||||
|
||||
const getChat: typeof chatApi.getChat = (): Chat | undefined => {
|
||||
const active = getActiveChat();
|
||||
// Copy so extensions cannot mutate another extension's descriptor.
|
||||
return active ? { ...active.chat } : undefined;
|
||||
};
|
||||
|
||||
const open: typeof chatApi.open = (): void => {
|
||||
const active = getActiveChat();
|
||||
// Open state only exists while a chat is registered; opening an empty slot
|
||||
// would otherwise leak `open` into a future, unrelated registration.
|
||||
if (panelOpen || !active) return;
|
||||
panelOpen = true;
|
||||
openEmitter.fire();
|
||||
notifyState();
|
||||
};
|
||||
|
||||
const close: typeof chatApi.close = (): void => {
|
||||
const active = getActiveChat();
|
||||
if (!panelOpen || !active) return;
|
||||
closePanel();
|
||||
notifyState();
|
||||
};
|
||||
|
||||
const isOpen: typeof chatApi.isOpen = (): boolean => panelOpen;
|
||||
|
||||
const getDisplayMode: typeof chatApi.getDisplayMode = (): DisplayMode =>
|
||||
modeEmitter.getCurrent();
|
||||
|
||||
const setDisplayMode: typeof chatApi.setDisplayMode = (
|
||||
displayMode: DisplayMode,
|
||||
): void => {
|
||||
if (displayMode === modeEmitter.getCurrent()) return;
|
||||
modeEmitter.fire(displayMode);
|
||||
notifyState();
|
||||
};
|
||||
|
||||
export const chat: typeof chatApi = {
|
||||
registerChat,
|
||||
getChat,
|
||||
onDidRegisterChat: registerEmitter.subscribe,
|
||||
onDidUnregisterChat: unregisterEmitter.subscribe,
|
||||
open,
|
||||
close,
|
||||
isOpen,
|
||||
onDidOpen: openEmitter.subscribe,
|
||||
onDidClose: closeEmitter.subscribe,
|
||||
getDisplayMode,
|
||||
setDisplayMode,
|
||||
onDidChangeDisplayMode: modeEmitter.subscribe,
|
||||
// The host fires this from its panel resizer; until that chrome exists the
|
||||
// event is exposed but never fires.
|
||||
onDidResizePanel: resizePanelEmitter.subscribe,
|
||||
};
|
||||
@@ -254,6 +254,33 @@ test('event listeners can be disposed', () => {
|
||||
expect(listener).toHaveBeenCalledTimes(1); // Still only 1 call
|
||||
});
|
||||
|
||||
test('handles errors in event listeners gracefully', () => {
|
||||
const manager = EditorProviders.getInstance();
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const errorListener = jest.fn(() => {
|
||||
throw new Error('Listener error');
|
||||
});
|
||||
const successListener = jest.fn();
|
||||
|
||||
manager.onDidRegister(errorListener);
|
||||
manager.onDidRegister(successListener);
|
||||
|
||||
manager.registerProvider(createMockEditor(), createMockEditorComponent());
|
||||
|
||||
// Both listeners should have been called
|
||||
expect(errorListener).toHaveBeenCalledTimes(1);
|
||||
expect(successListener).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Error should have been logged
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error in event listener:',
|
||||
expect.any(Error),
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('reset clears all providers and language mappings', () => {
|
||||
const manager = EditorProviders.getInstance();
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
import type { editors } from '@apache-superset/core';
|
||||
import { Disposable } from '../models';
|
||||
import { createEventEmitter } from '../utils';
|
||||
|
||||
type EditorLanguage = editors.EditorLanguage;
|
||||
type EditorProvider = editors.EditorProvider;
|
||||
@@ -28,8 +27,45 @@ type EditorComponent = editors.EditorComponent;
|
||||
type EditorRegisteredEvent = editors.EditorRegisteredEvent;
|
||||
type EditorUnregisteredEvent = editors.EditorUnregisteredEvent;
|
||||
|
||||
/**
|
||||
* Listener function type for events.
|
||||
*/
|
||||
type Listener<T> = (e: T) => void;
|
||||
|
||||
/**
|
||||
* Simple event emitter for editor provider lifecycle events.
|
||||
*/
|
||||
class EventEmitter<T> {
|
||||
private listeners: Set<Listener<T>> = new Set();
|
||||
|
||||
/**
|
||||
* Subscribe to this event.
|
||||
* @param listener The listener function to call when the event is fired.
|
||||
* @returns A Disposable to unsubscribe from the event.
|
||||
*/
|
||||
subscribe(listener: Listener<T>): Disposable {
|
||||
this.listeners.add(listener);
|
||||
return new Disposable(() => {
|
||||
this.listeners.delete(listener);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the event with the given data.
|
||||
* @param data The event data to pass to listeners.
|
||||
*/
|
||||
fire(data: T): void {
|
||||
this.listeners.forEach(listener => {
|
||||
try {
|
||||
listener(data);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error in event listener:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton manager for editor providers.
|
||||
* Handles registration, resolution, and lifecycle of custom editor implementations.
|
||||
@@ -47,9 +83,15 @@ class EditorProviders {
|
||||
*/
|
||||
private languageToProvider: Map<EditorLanguage, string> = new Map();
|
||||
|
||||
private registerEmitter = createEventEmitter<EditorRegisteredEvent>();
|
||||
/**
|
||||
* Event emitter for provider registration events.
|
||||
*/
|
||||
private registerEmitter = new EventEmitter<EditorRegisteredEvent>();
|
||||
|
||||
private unregisterEmitter = createEventEmitter<EditorUnregisteredEvent>();
|
||||
/**
|
||||
* Event emitter for provider unregistration events.
|
||||
*/
|
||||
private unregisterEmitter = new EventEmitter<EditorUnregisteredEvent>();
|
||||
|
||||
private syncListeners: Set<() => void> = new Set();
|
||||
|
||||
|
||||
@@ -27,13 +27,11 @@ export const core: typeof coreType = {
|
||||
};
|
||||
|
||||
export * from './authentication';
|
||||
export * from './chat';
|
||||
export * from './commands';
|
||||
export * from './editors';
|
||||
export * from './extensions';
|
||||
export * from './menus';
|
||||
export * from './models';
|
||||
export * from './navigation';
|
||||
export * from './sqlLab';
|
||||
export * from './utils';
|
||||
export * from './views';
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import type { menus as menusApi } from '@apache-superset/core';
|
||||
import { Disposable } from '../models';
|
||||
import { createEventEmitter } from '../utils';
|
||||
|
||||
type MenuItem = menusApi.MenuItem;
|
||||
type Menu = menusApi.Menu;
|
||||
@@ -48,19 +47,19 @@ const subscribe = (listener: () => void) => {
|
||||
return () => syncListeners.delete(listener);
|
||||
};
|
||||
|
||||
const registerEmitter = createEventEmitter<MenuItemRegisteredEvent>();
|
||||
const unregisterEmitter = createEventEmitter<MenuItemUnregisteredEvent>();
|
||||
const registerListeners = new Set<(e: MenuItemRegisteredEvent) => void>();
|
||||
const unregisterListeners = new Set<(e: MenuItemUnregisteredEvent) => void>();
|
||||
|
||||
const menuCache = new Map<string, Menu | undefined>();
|
||||
const notifyRegister = (event: MenuItemRegisteredEvent) => {
|
||||
menuCache.clear();
|
||||
syncListeners.forEach(l => l());
|
||||
registerEmitter.fire(event);
|
||||
registerListeners.forEach(l => l(event));
|
||||
};
|
||||
const notifyUnregister = (event: MenuItemUnregisteredEvent) => {
|
||||
menuCache.clear();
|
||||
syncListeners.forEach(l => l());
|
||||
unregisterEmitter.fire(event);
|
||||
unregisterListeners.forEach(l => l(event));
|
||||
};
|
||||
|
||||
const registerMenuItem: typeof menusApi.registerMenuItem = (
|
||||
@@ -118,11 +117,16 @@ export const useMenu = (location: string): Menu | undefined =>
|
||||
|
||||
export const onDidRegisterMenuItem: typeof menusApi.onDidRegisterMenuItem = (
|
||||
listener: (e: MenuItemRegisteredEvent) => void,
|
||||
): Disposable => registerEmitter.subscribe(listener);
|
||||
): Disposable => {
|
||||
registerListeners.add(listener);
|
||||
return new Disposable(() => registerListeners.delete(listener));
|
||||
};
|
||||
|
||||
export const onDidUnregisterMenuItem: typeof menusApi.onDidUnregisterMenuItem =
|
||||
(listener: (e: MenuItemUnregisteredEvent) => void): Disposable =>
|
||||
unregisterEmitter.subscribe(listener);
|
||||
(listener: (e: MenuItemUnregisteredEvent) => void): Disposable => {
|
||||
unregisterListeners.add(listener);
|
||||
return new Disposable(() => unregisterListeners.delete(listener));
|
||||
};
|
||||
|
||||
export const menus: typeof menusApi = {
|
||||
registerMenuItem,
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Reset module state between tests so currentPage is re-initialized.
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: { pathname: '/' },
|
||||
});
|
||||
});
|
||||
|
||||
async function importNavigation() {
|
||||
const mod = await import('./index');
|
||||
return mod;
|
||||
}
|
||||
|
||||
test('getPage falls back to "home" for the welcome page and unknown pathnames', async () => {
|
||||
const { navigation, notifyPageChange } = await importNavigation();
|
||||
// The default pathname ('/') is not enumerated and falls back to home.
|
||||
expect(navigation.getPage()).toBe('home');
|
||||
notifyPageChange('/superset/welcome/');
|
||||
expect(navigation.getPage()).toBe('home');
|
||||
});
|
||||
|
||||
test('getPage derives the page from window.location.pathname', async () => {
|
||||
window.location.pathname = '/superset/dashboard/42/';
|
||||
const { navigation } = await importNavigation();
|
||||
expect(navigation.getPage()).toBe('dashboard');
|
||||
});
|
||||
|
||||
test('notifyPageChange updates the current page type', async () => {
|
||||
const { navigation, notifyPageChange } = await importNavigation();
|
||||
notifyPageChange('/explore/?form_data={}');
|
||||
expect(navigation.getPage()).toBe('explore');
|
||||
});
|
||||
|
||||
test('notifyPageChange fires listeners on page type change', async () => {
|
||||
const { navigation, notifyPageChange } = await importNavigation();
|
||||
const listener = jest.fn();
|
||||
const disposable = navigation.onDidChangePage(listener);
|
||||
|
||||
notifyPageChange('/superset/dashboard/1/');
|
||||
expect(listener).toHaveBeenCalledWith('dashboard');
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('notifyPageChange does not fire listeners when page type is unchanged', async () => {
|
||||
window.location.pathname = '/superset/dashboard/1/';
|
||||
const { navigation, notifyPageChange } = await importNavigation();
|
||||
const listener = jest.fn();
|
||||
navigation.onDidChangePage(listener);
|
||||
|
||||
notifyPageChange('/superset/dashboard/2/');
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('onDidChangePage listener is removed after dispose', async () => {
|
||||
const { navigation, notifyPageChange } = await importNavigation();
|
||||
const listener = jest.fn();
|
||||
const disposable = navigation.onDidChangePage(listener);
|
||||
|
||||
disposable.dispose();
|
||||
notifyPageChange('/superset/dashboard/1/');
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('sqllab path is matched with and without trailing slash', async () => {
|
||||
const { notifyPageChange, navigation } = await importNavigation();
|
||||
notifyPageChange('/sqllab');
|
||||
expect(navigation.getPage()).toBe('sqllab');
|
||||
notifyPageChange('/explore/');
|
||||
notifyPageChange('/sqllab/');
|
||||
expect(navigation.getPage()).toBe('sqllab');
|
||||
});
|
||||
|
||||
test('chart and dashboard list pages get their own page types', async () => {
|
||||
const { notifyPageChange, navigation } = await importNavigation();
|
||||
notifyPageChange('/chart/list/');
|
||||
expect(navigation.getPage()).toBe('chart_list');
|
||||
notifyPageChange('/dashboard/list/');
|
||||
expect(navigation.getPage()).toBe('dashboard_list');
|
||||
});
|
||||
|
||||
test('dataset list and single-dataset pages get distinct page types', async () => {
|
||||
const { notifyPageChange, navigation } = await importNavigation();
|
||||
notifyPageChange('/tablemodelview/list/');
|
||||
expect(navigation.getPage()).toBe('dataset_list');
|
||||
notifyPageChange('/dataset/42');
|
||||
expect(navigation.getPage()).toBe('dataset');
|
||||
});
|
||||
|
||||
test('sqllab editor, query history, and saved queries get distinct page types', async () => {
|
||||
const { notifyPageChange, navigation } = await importNavigation();
|
||||
notifyPageChange('/sqllab/');
|
||||
expect(navigation.getPage()).toBe('sqllab');
|
||||
notifyPageChange('/sqllab/history/');
|
||||
expect(navigation.getPage()).toBe('query_history');
|
||||
notifyPageChange('/savedqueryview/list/');
|
||||
expect(navigation.getPage()).toBe('saved_queries');
|
||||
});
|
||||
|
||||
test('chart/add resolves to explore, not chart_list', async () => {
|
||||
const { notifyPageChange, navigation } = await importNavigation();
|
||||
notifyPageChange('/chart/add');
|
||||
expect(navigation.getPage()).toBe('explore');
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Host-internal implementation of the `navigation` namespace.
|
||||
*
|
||||
* Backed by browser location — no Redux dependency.
|
||||
* The app shell calls `notifyPageChange(pathname)` whenever the route changes.
|
||||
*/
|
||||
|
||||
import type { navigation as navigationApi } from '@apache-superset/core';
|
||||
import { Disposable } from '../models';
|
||||
import { createEventEmitter } from '../utils';
|
||||
|
||||
type Page = navigationApi.Page;
|
||||
|
||||
const pageChangeEmitter = createEventEmitter<Page>();
|
||||
|
||||
function derivePage(pathname: string): Page {
|
||||
if (pathname.startsWith('/superset/dashboard/')) return 'dashboard';
|
||||
if (pathname.startsWith('/dashboard/list')) return 'dashboard_list';
|
||||
if (pathname.startsWith('/explore/')) return 'explore';
|
||||
if (pathname.startsWith('/superset/explore/')) return 'explore';
|
||||
if (pathname.startsWith('/chart/add')) return 'explore';
|
||||
if (pathname.startsWith('/chart/list')) return 'chart_list';
|
||||
if (pathname.startsWith('/sqllab/history')) return 'query_history';
|
||||
if (pathname.startsWith('/savedqueryview/list')) return 'saved_queries';
|
||||
if (pathname === '/sqllab' || pathname.startsWith('/sqllab/'))
|
||||
return 'sqllab';
|
||||
if (pathname.startsWith('/tablemodelview/list')) return 'dataset_list';
|
||||
if (pathname.startsWith('/dataset/')) return 'dataset';
|
||||
// The welcome page and any route not explicitly enumerated fall back to home.
|
||||
return 'home';
|
||||
}
|
||||
|
||||
let currentPage: Page | undefined;
|
||||
|
||||
function getOrInitPage(): Page {
|
||||
if (currentPage === undefined) {
|
||||
currentPage = derivePage(window.location.pathname);
|
||||
}
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
/** Called by ExtensionsStartup whenever the React Router location changes. */
|
||||
export const notifyPageChange = (pathname: string): void => {
|
||||
const next = derivePage(pathname);
|
||||
if (next === getOrInitPage()) return;
|
||||
currentPage = next;
|
||||
pageChangeEmitter.fire(next);
|
||||
};
|
||||
|
||||
const getPage: typeof navigationApi.getPage = () => getOrInitPage();
|
||||
|
||||
const onDidChangePage: typeof navigationApi.onDidChangePage = (
|
||||
listener: (page: Page) => void,
|
||||
thisArgs?: any,
|
||||
): Disposable => pageChangeEmitter.subscribe(listener, thisArgs);
|
||||
|
||||
export const navigation: typeof navigationApi = {
|
||||
getPage,
|
||||
onDidChangePage,
|
||||
};
|
||||
@@ -21,57 +21,6 @@ import { AnyAction } from 'redux';
|
||||
import { listenerMiddleware, RootState, store } from 'src/views/store';
|
||||
import { AnyListenerPredicate } from '@reduxjs/toolkit';
|
||||
|
||||
type Listener<T> = (e: T) => unknown;
|
||||
|
||||
/** A stateless event emitter exposing a VS Code-style `event` subscriber. */
|
||||
export interface EventEmitter<T> {
|
||||
/** Notifies every current subscriber with `value`. */
|
||||
fire(value: T): void;
|
||||
/** Registers a listener; returns a Disposable that removes it. */
|
||||
subscribe: core.Event<T>;
|
||||
}
|
||||
|
||||
/** An event emitter that also retains the last fired value. */
|
||||
export interface ValueEventEmitter<T> extends EventEmitter<T> {
|
||||
/** Returns the value last passed to {@link fire} (or the initial value). */
|
||||
getCurrent(): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a stateless event emitter. Listeners registered via `event` receive
|
||||
* every subsequent `fire`; a returned Disposable removes the listener.
|
||||
*/
|
||||
export function createEventEmitter<T>(): EventEmitter<T> {
|
||||
const listeners = new Set<Listener<T>>();
|
||||
const subscribe: core.Event<T> = (listener, thisArgs) => {
|
||||
const bound = thisArgs ? listener.bind(thisArgs) : listener;
|
||||
listeners.add(bound);
|
||||
return { dispose: () => listeners.delete(bound) };
|
||||
};
|
||||
return {
|
||||
fire: value => listeners.forEach(fn => fn(value)),
|
||||
subscribe,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a value event emitter seeded with `initial`. Behaves like
|
||||
* {@link createEventEmitter} but also tracks the last fired value, readable
|
||||
* via `getCurrent` — useful for state that is both observed and queried.
|
||||
*/
|
||||
export function createValueEventEmitter<T>(initial: T): ValueEventEmitter<T> {
|
||||
const { fire, subscribe } = createEventEmitter<T>();
|
||||
let current = initial;
|
||||
return {
|
||||
fire: value => {
|
||||
current = value;
|
||||
fire(value);
|
||||
},
|
||||
subscribe,
|
||||
getCurrent: () => current,
|
||||
};
|
||||
}
|
||||
|
||||
export function createActionListener<V>(
|
||||
predicate: AnyListenerPredicate<RootState>,
|
||||
listener: (v: V) => void,
|
||||
|
||||
@@ -29,7 +29,6 @@ import type { views as viewsApi } from '@apache-superset/core';
|
||||
import { ErrorBoundary } from 'src/components/ErrorBoundary';
|
||||
import ExtensionPlaceholder from 'src/extensions/ExtensionPlaceholder';
|
||||
import { Disposable } from '../models';
|
||||
import { createEventEmitter } from '../utils';
|
||||
|
||||
type View = viewsApi.View;
|
||||
type ViewRegisteredEvent = viewsApi.ViewRegisteredEvent;
|
||||
@@ -48,19 +47,19 @@ const subscribe = (listener: () => void) => {
|
||||
return () => syncListeners.delete(listener);
|
||||
};
|
||||
|
||||
const registerEmitter = createEventEmitter<ViewRegisteredEvent>();
|
||||
const unregisterEmitter = createEventEmitter<ViewUnregisteredEvent>();
|
||||
const registerListeners = new Set<(e: ViewRegisteredEvent) => void>();
|
||||
const unregisterListeners = new Set<(e: ViewUnregisteredEvent) => void>();
|
||||
|
||||
const viewsCache = new Map<string, View[] | undefined>();
|
||||
const notifyRegister = (event: ViewRegisteredEvent) => {
|
||||
viewsCache.clear();
|
||||
syncListeners.forEach(l => l());
|
||||
registerEmitter.fire(event);
|
||||
registerListeners.forEach(l => l(event));
|
||||
};
|
||||
const notifyUnregister = (event: ViewUnregisteredEvent) => {
|
||||
viewsCache.clear();
|
||||
syncListeners.forEach(l => l());
|
||||
unregisterEmitter.fire(event);
|
||||
unregisterListeners.forEach(l => l(event));
|
||||
};
|
||||
|
||||
const registerView: typeof viewsApi.registerView = (
|
||||
@@ -117,11 +116,17 @@ export const useViews = (location: string): View[] | undefined =>
|
||||
|
||||
export const onDidRegisterView: typeof viewsApi.onDidRegisterView = (
|
||||
listener: (e: ViewRegisteredEvent) => void,
|
||||
): Disposable => registerEmitter.subscribe(listener);
|
||||
): Disposable => {
|
||||
registerListeners.add(listener);
|
||||
return new Disposable(() => registerListeners.delete(listener));
|
||||
};
|
||||
|
||||
export const onDidUnregisterView: typeof viewsApi.onDidUnregisterView = (
|
||||
listener: (e: ViewUnregisteredEvent) => void,
|
||||
): Disposable => unregisterEmitter.subscribe(listener);
|
||||
): Disposable => {
|
||||
unregisterListeners.add(listener);
|
||||
return new Disposable(() => unregisterListeners.delete(listener));
|
||||
};
|
||||
|
||||
export const views: typeof viewsApi = {
|
||||
registerView,
|
||||
|
||||
@@ -193,52 +193,57 @@ export default function getControlItemsMap({
|
||||
t('Populate "Default value" to enable this control')
|
||||
}
|
||||
>
|
||||
<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();
|
||||
}}
|
||||
{/* 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}
|
||||
>
|
||||
<>
|
||||
{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>
|
||||
<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>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,9 @@ const ColorBreakpointsPopoverTrigger = ({
|
||||
onOpenChange={setVisibility}
|
||||
destroyOnHidden
|
||||
>
|
||||
{props.children}
|
||||
{/* Wrap in span so the Popover can attach a ref without relying
|
||||
on findDOMNode (deprecated in React 18+). */}
|
||||
<span>{props.children}</span>
|
||||
</ControlPopover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,7 +52,9 @@ const ContourPopoverTrigger = ({
|
||||
onOpenChange={setVisibility}
|
||||
destroyOnHidden
|
||||
>
|
||||
{props.children}
|
||||
{/* Wrap in span so the Popover can attach a ref without relying
|
||||
on findDOMNode (deprecated in React 18+). */}
|
||||
<span>{props.children}</span>
|
||||
</ControlPopover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -369,16 +369,21 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
|
||||
overlayClassName="time-range-popover"
|
||||
>
|
||||
<Tooltip placement="top" title={tooltipTitle}>
|
||||
<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}
|
||||
/>
|
||||
{/* 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>
|
||||
</Tooltip>
|
||||
</ControlPopover>
|
||||
);
|
||||
|
||||
@@ -201,7 +201,9 @@ const ColumnSelectPopoverTriggerInner = ({
|
||||
title={popoverTitle}
|
||||
destroyOnHidden
|
||||
>
|
||||
{children}
|
||||
{/* Wrap in span so the Popover can attach a ref without relying
|
||||
on findDOMNode (deprecated in React 18+). */}
|
||||
<span>{children}</span>
|
||||
</ControlPopover>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -104,14 +104,6 @@ interface AdhocFilterControlState {
|
||||
|
||||
const { warning } = Modal;
|
||||
|
||||
const defaultProps = {
|
||||
name: '',
|
||||
onChange: () => {},
|
||||
columns: [],
|
||||
savedMetrics: [],
|
||||
selectedMetrics: [],
|
||||
};
|
||||
|
||||
function optionsForSelect(props: AdhocFilterControlProps): FilterOption[] {
|
||||
const options = [
|
||||
...(props.columns || []),
|
||||
@@ -391,7 +383,7 @@ class AdhocFilterControl extends Component<
|
||||
return (
|
||||
<div className="metrics-select" data-test="adhoc-filter-control">
|
||||
<HeaderContainer>
|
||||
<ControlHeader {...this.props} />
|
||||
<ControlHeader {...this.props} name={this.props.name ?? ''} />
|
||||
</HeaderContainer>
|
||||
<LabelsContainer>
|
||||
{[
|
||||
@@ -413,7 +405,4 @@ class AdhocFilterControl extends Component<
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error - defaultProps for backward compatibility
|
||||
AdhocFilterControl.defaultProps = defaultProps;
|
||||
|
||||
export default withTheme(AdhocFilterControl);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user