mirror of
https://github.com/apache/superset.git
synced 2026-06-18 22:19:21 +00:00
Compare commits
50 Commits
fix/report
...
fix/helm-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c1ef9398e | ||
|
|
492b7727b2 | ||
|
|
7c0d888ac5 | ||
|
|
5e812c8757 | ||
|
|
de390f22a4 | ||
|
|
464c67d586 | ||
|
|
7f7f87e823 | ||
|
|
7c2f5142ce | ||
|
|
874ac3dc01 | ||
|
|
f56e34d6e6 | ||
|
|
742a21f6f7 | ||
|
|
a7c49ac9f2 | ||
|
|
99d927eac7 | ||
|
|
994594e4a8 | ||
|
|
e92599fb50 | ||
|
|
eebe1a1a5b | ||
|
|
664e777a84 | ||
|
|
750518cf6f | ||
|
|
59d1b5f300 | ||
|
|
a27ec1923e | ||
|
|
3e2174b50f | ||
|
|
5b66443d48 | ||
|
|
2ea7585490 | ||
|
|
eeac76146c | ||
|
|
6a1091d576 | ||
|
|
8e82b6b2c3 | ||
|
|
b0c5f99007 | ||
|
|
f1ae683923 | ||
|
|
d51d98891e | ||
|
|
1f95a6c486 | ||
|
|
e93cbd6c38 | ||
|
|
dca8af770c | ||
|
|
81c1181519 | ||
|
|
387c62919e | ||
|
|
77d7483f27 | ||
|
|
1a8d08152d | ||
|
|
257dafeec5 | ||
|
|
6d08e79259 | ||
|
|
01ed81785e | ||
|
|
7b4efacbc2 | ||
|
|
7cb4990403 | ||
|
|
c90b2571d7 | ||
|
|
1a4941eee5 | ||
|
|
d839cca995 | ||
|
|
0ec7e7df99 | ||
|
|
9d8287e1bd | ||
|
|
0c696cea7e | ||
|
|
fe625a917e | ||
|
|
a69f9eb00d | ||
|
|
1311d040ba |
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)..."
|
||||
|
||||
@@ -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",
|
||||
@@ -101,15 +101,15 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.60.1",
|
||||
"@typescript-eslint/parser": "^8.61.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier": "^3.8.4",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.60.1",
|
||||
"typescript-eslint": "^8.61.0",
|
||||
"webpack": "^5.107.2"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
@@ -7235,10 +7235,10 @@
|
||||
"pypi_packages": [
|
||||
"oracledb"
|
||||
],
|
||||
"connection_string": "oracle://{username}:{password}@{hostname}:{port}",
|
||||
"connection_string": "oracle+oracledb://{username}:{password}@{hostname}:{port}",
|
||||
"default_port": 1521,
|
||||
"notes": "Previously used cx_Oracle, now uses oracledb.",
|
||||
"docs_url": "https://cx-oracle.readthedocs.io/en/latest/user_guide/installation.html",
|
||||
"docs_url": "https://python-oracledb.readthedocs.io/en/latest/user_guide/installation.html",
|
||||
"category": "Other Databases"
|
||||
},
|
||||
"engine": "oracle",
|
||||
|
||||
294
docs/yarn.lock
294
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"
|
||||
@@ -4922,110 +4922,110 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.60.1", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz#c1060bb8fa4be80624d3f3dec8dd9caca373af76"
|
||||
integrity sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==
|
||||
"@typescript-eslint/eslint-plugin@8.61.0", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz#db20271974b94a3a54d3b9544e5f5b3481448400"
|
||||
integrity sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.60.1"
|
||||
"@typescript-eslint/type-utils" "8.60.1"
|
||||
"@typescript-eslint/utils" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
"@typescript-eslint/scope-manager" "8.61.0"
|
||||
"@typescript-eslint/type-utils" "8.61.0"
|
||||
"@typescript-eslint/utils" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.60.1", "@typescript-eslint/parser@^8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289"
|
||||
integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==
|
||||
"@typescript-eslint/parser@8.61.0", "@typescript-eslint/parser@^8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.0.tgz#1afe73c9ccce16b7a26d6b95f9400b0ccc34af87"
|
||||
integrity sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.60.1"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
"@typescript-eslint/scope-manager" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz#eb29712f58d72c222fc727162e92f2ab4670971b"
|
||||
integrity sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==
|
||||
"@typescript-eslint/project-service@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz#417a2feac32e8ebd336d63f068c3b42b736ea1ac"
|
||||
integrity sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.60.1"
|
||||
"@typescript-eslint/types" "^8.60.1"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.61.0"
|
||||
"@typescript-eslint/types" "^8.61.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz#2f875962eaad0a0789cc3c36aea9b4ddeb2dd9c8"
|
||||
integrity sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==
|
||||
"@typescript-eslint/scope-manager@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz#93c2520d05653fe65eb9ee98efc74fd0134a7852"
|
||||
integrity sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
|
||||
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@^8.60.1":
|
||||
"@typescript-eslint/tsconfig-utils@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba"
|
||||
integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==
|
||||
|
||||
"@typescript-eslint/type-utils@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz#1ae45f0f2a701354beea4a58c2161e40a5e3c379"
|
||||
integrity sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==
|
||||
"@typescript-eslint/tsconfig-utils@^8.61.0":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz#ca88080e0cf191d49516d7f300b67aa090d2254f"
|
||||
integrity sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==
|
||||
|
||||
"@typescript-eslint/type-utils@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz#50219b57e6b89cecfb1a15f093b15ec9ee019974"
|
||||
integrity sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/utils" "8.60.1"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
"@typescript-eslint/utils" "8.61.0"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/types@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4"
|
||||
integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==
|
||||
|
||||
"@typescript-eslint/types@^8.60.1":
|
||||
"@typescript-eslint/types@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d"
|
||||
integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz#016630b119228bf483ddc652703a6a038f3fdd74"
|
||||
integrity sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==
|
||||
"@typescript-eslint/types@^8.61.0":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.1.tgz#0c51f518e4e6848371a1c988e859d59eb7522d5a"
|
||||
integrity sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz#98ca47260bbf627fc28f018b3a0abf00e3090690"
|
||||
integrity sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.60.1"
|
||||
"@typescript-eslint/tsconfig-utils" "8.60.1"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
"@typescript-eslint/project-service" "8.61.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
||||
debug "^4.4.3"
|
||||
minimatch "^10.2.2"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/utils@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.1.tgz#31cf566095602d9fe8ad91837d2eb520b8de762b"
|
||||
integrity sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==
|
||||
"@typescript-eslint/utils@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f"
|
||||
integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.60.1"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/scope-manager" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz#165d1d8901137b944efaf18f00ab5ecb57f06995"
|
||||
integrity sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==
|
||||
"@typescript-eslint/visitor-keys@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz#39b4e1ab8936d23bea973d39fd092f9aa21f275e"
|
||||
integrity sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -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"
|
||||
@@ -14499,15 +14499,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.60.1:
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.1.tgz#13db05c6eabb89669deec44545b788a0e9aee640"
|
||||
integrity sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==
|
||||
typescript-eslint@^8.61.0:
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz#6927fb94f5f29623e370d33fd9fa61f15d6d996b"
|
||||
integrity sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.60.1"
|
||||
"@typescript-eslint/parser" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/utils" "8.60.1"
|
||||
"@typescript-eslint/eslint-plugin" "8.61.0"
|
||||
"@typescript-eslint/parser" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
"@typescript-eslint/utils" "8.61.0"
|
||||
|
||||
typescript@~6.0.3:
|
||||
version "6.0.3"
|
||||
|
||||
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.16.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.17.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
@@ -111,9 +111,6 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
||||
| init.resources | object | `{}` | |
|
||||
| init.tolerations | list | `[]` | |
|
||||
| init.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to init job |
|
||||
| initImage.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| initImage.repository | string | `"apache/superset"` | |
|
||||
| initImage.tag | string | `"dockerize"` | |
|
||||
| nameOverride | string | `nil` | Provide a name to override the name of the chart |
|
||||
| nodeSelector | object | `{}` | |
|
||||
| postgresql | object | see `values.yaml` | Configuration values for the postgresql dependency. ref: https://github.com/bitnami/charts/tree/main/bitnami/postgresql |
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -62,6 +62,9 @@ spec:
|
||||
{{- if .Values.init.initContainers }}
|
||||
initContainers: {{- tpl (toYaml .Values.init.initContainers) . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with .Values.hostAliases }}
|
||||
hostAliases: {{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ template "superset.name" . }}-init-db
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
@@ -101,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 }}
|
||||
|
||||
@@ -194,11 +194,6 @@ image:
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
initImage:
|
||||
repository: apache/superset
|
||||
tag: dockerize
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8088
|
||||
@@ -303,15 +298,29 @@ supersetNode:
|
||||
# @default -- a container waiting for postgres
|
||||
initContainers:
|
||||
- name: wait-for-postgres
|
||||
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
|
||||
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: "{{ tpl .Values.envFromSecret . }}"
|
||||
command:
|
||||
- /bin/sh
|
||||
- /bin/bash
|
||||
- -c
|
||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s
|
||||
- |
|
||||
# opening a /dev/tcp fd performs a TCP connect without sending any
|
||||
# payload (avoids postgres "incomplete startup packet" log noise);
|
||||
# no external `dockerize`, `nc`, or busybox needed. SECONDS-based
|
||||
# deadline mirrors the prior `dockerize -timeout 120s` behaviour.
|
||||
SECONDS=0
|
||||
until (exec 3<>/dev/tcp/"$DB_HOST"/"$DB_PORT") 2>/dev/null; do
|
||||
if [ "$SECONDS" -ge 120 ]; then
|
||||
echo "timeout waiting for postgres at $DB_HOST:$DB_PORT after 120s" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "waiting for postgres at $DB_HOST:$DB_PORT (elapsed ${SECONDS}s)"
|
||||
sleep 2
|
||||
done
|
||||
echo "postgres at $DB_HOST:$DB_PORT is up"
|
||||
resources:
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
@@ -407,15 +416,31 @@ supersetWorker:
|
||||
# @default -- a container waiting for postgres and redis
|
||||
initContainers:
|
||||
- name: wait-for-postgres-redis
|
||||
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
|
||||
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: "{{ tpl .Values.envFromSecret . }}"
|
||||
command:
|
||||
- /bin/sh
|
||||
- /bin/bash
|
||||
- -c
|
||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
|
||||
- |
|
||||
# See supersetNode.initContainers for the rationale.
|
||||
SECONDS=0
|
||||
wait_for() {
|
||||
local host=$1 port=$2 name=$3
|
||||
until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
|
||||
if [ "$SECONDS" -ge 120 ]; then
|
||||
echo "timeout waiting for $name at $host:$port after 120s" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "waiting for $name at $host:$port (elapsed ${SECONDS}s)"
|
||||
sleep 2
|
||||
done
|
||||
echo "$name at $host:$port is up"
|
||||
}
|
||||
wait_for "$DB_HOST" "$DB_PORT" postgres
|
||||
wait_for "$REDIS_HOST" "$REDIS_PORT" redis
|
||||
resources:
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
@@ -495,15 +520,31 @@ supersetCeleryBeat:
|
||||
# @default -- a container waiting for postgres
|
||||
initContainers:
|
||||
- name: wait-for-postgres-redis
|
||||
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
|
||||
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: "{{ tpl .Values.envFromSecret . }}"
|
||||
command:
|
||||
- /bin/sh
|
||||
- /bin/bash
|
||||
- -c
|
||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
|
||||
- |
|
||||
# See supersetNode.initContainers for the rationale.
|
||||
SECONDS=0
|
||||
wait_for() {
|
||||
local host=$1 port=$2 name=$3
|
||||
until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
|
||||
if [ "$SECONDS" -ge 120 ]; then
|
||||
echo "timeout waiting for $name at $host:$port after 120s" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "waiting for $name at $host:$port (elapsed ${SECONDS}s)"
|
||||
sleep 2
|
||||
done
|
||||
echo "$name at $host:$port is up"
|
||||
}
|
||||
wait_for "$DB_HOST" "$DB_PORT" postgres
|
||||
wait_for "$REDIS_HOST" "$REDIS_PORT" redis
|
||||
resources:
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
@@ -594,15 +635,31 @@ supersetCeleryFlower:
|
||||
# @default -- a container waiting for postgres and redis
|
||||
initContainers:
|
||||
- name: wait-for-postgres-redis
|
||||
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
|
||||
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: "{{ tpl .Values.envFromSecret . }}"
|
||||
command:
|
||||
- /bin/sh
|
||||
- /bin/bash
|
||||
- -c
|
||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
|
||||
- |
|
||||
# See supersetNode.initContainers for the rationale.
|
||||
SECONDS=0
|
||||
wait_for() {
|
||||
local host=$1 port=$2 name=$3
|
||||
until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
|
||||
if [ "$SECONDS" -ge 120 ]; then
|
||||
echo "timeout waiting for $name at $host:$port after 120s" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "waiting for $name at $host:$port (elapsed ${SECONDS}s)"
|
||||
sleep 2
|
||||
done
|
||||
echo "$name at $host:$port is up"
|
||||
}
|
||||
wait_for "$DB_HOST" "$DB_PORT" postgres
|
||||
wait_for "$REDIS_HOST" "$REDIS_PORT" redis
|
||||
resources:
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
@@ -764,15 +821,26 @@ init:
|
||||
# @default -- a container waiting for postgres
|
||||
initContainers:
|
||||
- name: wait-for-postgres
|
||||
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
|
||||
imagePullPolicy: "{{ .Values.initImage.pullPolicy }}"
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: "{{ tpl .Values.envFromSecret . }}"
|
||||
command:
|
||||
- /bin/sh
|
||||
- /bin/bash
|
||||
- -c
|
||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s
|
||||
- |
|
||||
# See supersetNode.initContainers for the rationale.
|
||||
SECONDS=0
|
||||
until (exec 3<>/dev/tcp/"$DB_HOST"/"$DB_PORT") 2>/dev/null; do
|
||||
if [ "$SECONDS" -ge 120 ]; then
|
||||
echo "timeout waiting for postgres at $DB_HOST:$DB_PORT after 120s" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "waiting for postgres at $DB_HOST:$DB_PORT (elapsed ${SECONDS}s)"
|
||||
sleep 2
|
||||
done
|
||||
echo "postgres at $DB_HOST:$DB_PORT is up"
|
||||
resources:
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
|
||||
@@ -43,7 +43,7 @@ dependencies = [
|
||||
"click-option-group",
|
||||
"colorama",
|
||||
"flask-cors>=6.0.0, <7.0",
|
||||
"croniter>=0.3.28",
|
||||
"croniter>=6.2.2",
|
||||
"cron-descriptor",
|
||||
"cryptography>=42.0.4, <47.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
@@ -53,7 +53,7 @@ dependencies = [
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
"flask-login>=0.6.0, < 1.0",
|
||||
"flask-migrate>=3.1.0, <5.0",
|
||||
"flask-migrate>=4.1.0, <5.0",
|
||||
"flask-session>=0.4.0, <1.0",
|
||||
"flask-wtf>=1.3.0, <2.0",
|
||||
"geopy",
|
||||
@@ -97,7 +97,7 @@ dependencies = [
|
||||
"selenium>=4.44.0, <5.0",
|
||||
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
|
||||
"sshtunnel>=0.4.0, <0.5",
|
||||
"simplejson>=3.15.0",
|
||||
"simplejson>=4.1.1",
|
||||
"slack_sdk>=3.19.0, <4",
|
||||
"sqlalchemy>=1.4, <2",
|
||||
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
|
||||
@@ -144,7 +144,7 @@ dynamodb = ["pydynamodb>=0.4.2"]
|
||||
solr = ["sqlalchemy-solr >= 0.2.0"]
|
||||
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
|
||||
exasol = ["sqlalchemy-exasol>=2.4.0, <8.0"]
|
||||
excel = ["xlrd>=1.2.0, <1.3"]
|
||||
excel = ["xlrd>=2.0.2, <2.1"]
|
||||
fastmcp = [
|
||||
"fastmcp>=3.2.4,<4.0",
|
||||
# tiktoken backs the response-size-guard token estimator. Without
|
||||
@@ -156,7 +156,7 @@ firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
|
||||
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
|
||||
gevent = ["gevent>=26.4.0"]
|
||||
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
|
||||
hana = ["hdbcli==2.28.20", "sqlalchemy_hana==0.4.0"]
|
||||
hana = ["hdbcli==2.28.21", "sqlalchemy_hana==0.4.0"]
|
||||
hive = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7.0",
|
||||
@@ -173,11 +173,11 @@ motherduck = ["apache-superset[duckdb]"]
|
||||
mysql = ["mysqlclient>=2.1.0, <3"]
|
||||
ocient = [
|
||||
"sqlalchemy-ocient>=1.0.0",
|
||||
"pyocient>=1.0.15, <2",
|
||||
"pyocient>=1.0.15, <4",
|
||||
"shapely",
|
||||
"geojson",
|
||||
]
|
||||
oracle = ["cx-Oracle>8.0.0, <8.4"]
|
||||
oracle = ["oracledb>=2.0.0, <5"]
|
||||
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
|
||||
pinot = ["pinotdb>=5.0.0, <10.0.0"]
|
||||
playwright = ["playwright>=1.60.0, <2"]
|
||||
|
||||
@@ -84,7 +84,7 @@ colorama==0.4.6
|
||||
# flask-appbuilder
|
||||
cron-descriptor==1.4.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
croniter==6.0.0
|
||||
croniter==6.2.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
cryptography==46.0.7
|
||||
# via
|
||||
@@ -141,7 +141,7 @@ flask-login==0.6.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
flask-migrate==3.1.0
|
||||
flask-migrate==4.1.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-session==0.8.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -384,7 +384,7 @@ setuptools==80.9.0
|
||||
# via -r requirements/base.in
|
||||
shillelagh==1.4.4
|
||||
# via apache-superset (pyproject.toml)
|
||||
simplejson==3.20.1
|
||||
simplejson==4.1.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
six==1.17.0
|
||||
# via
|
||||
|
||||
@@ -174,7 +174,7 @@ cron-descriptor==1.4.5
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
croniter==6.0.0
|
||||
croniter==6.2.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -293,7 +293,7 @@ flask-login==0.6.3
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
flask-migrate==3.1.0
|
||||
flask-migrate==4.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -939,7 +939,7 @@ shillelagh==1.4.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
simplejson==3.20.1
|
||||
simplejson==4.1.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
575
superset-frontend/package-lock.json
generated
575
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -178,14 +178,14 @@
|
||||
"echarts": "^5.6.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.3.5",
|
||||
"fuse.js": "^7.4.1",
|
||||
"fuse.js": "^7.4.2",
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
"geostyler-data": "^1.1.0",
|
||||
"geostyler-openlayers-parser": "^5.7.0",
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"google-auth-library": "^10.7.0",
|
||||
"immer": "^11.1.8",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
@@ -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",
|
||||
@@ -261,16 +261,16 @@
|
||||
"@babel/types": "^7.29.7",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@formatjs/intl-durationformat": "^0.10.13",
|
||||
"@formatjs/intl-durationformat": "^0.10.14",
|
||||
"@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",
|
||||
@@ -344,18 +344,19 @@
|
||||
"lightningcss": "^1.32.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.68.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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -97,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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"dompurify": "^3.4.8",
|
||||
"dompurify": "^3.4.9",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"jed": "^1.1.1",
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.18.1",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"dompurify": "^3.4.8",
|
||||
"dompurify": "^3.4.9",
|
||||
"prop-types": "^15.8.1",
|
||||
"urijs": "^1.19.11"
|
||||
},
|
||||
@@ -43,6 +43,6 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"dayjs": "^1.11.21",
|
||||
"react": "^18.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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -21,6 +21,7 @@ import { css, styled } from '@apache-superset/core/theme';
|
||||
export default styled.div`
|
||||
${({ theme }) => css`
|
||||
/* Base table styles */
|
||||
padding: ${theme.sizeUnit * 5}px;
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
|
||||
@@ -1613,8 +1613,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
pageSize={pageSize}
|
||||
serverPaginationData={serverPaginationData}
|
||||
pageSizeOptions={pageSizeOptions}
|
||||
width={widthFromState}
|
||||
height={heightFromState}
|
||||
width={Math.max(0, widthFromState - theme.sizeUnit * 10)}
|
||||
height={Math.max(0, heightFromState - theme.sizeUnit * 10)}
|
||||
serverPagination={serverPagination}
|
||||
onServerPaginationChange={handleServerPaginationChange}
|
||||
onColumnOrderChange={() => setColumnOrderToggle(!columnOrderToggle)}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
import type { ReactElement } from 'react';
|
||||
import type { ControlPanelSectionConfig } from '@superset-ui/chart-controls';
|
||||
import type {
|
||||
ControlPanelSectionConfig,
|
||||
CustomControlItem,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { isCustomControlItem } from '@superset-ui/chart-controls';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { render } from '@testing-library/react';
|
||||
import { SqlaFormData } from '@superset-ui/core';
|
||||
@@ -28,6 +32,7 @@ import DeckGLGeoJson, {
|
||||
computeGeoJsonIconOptionsFromJsOutput,
|
||||
computeGeoJsonIconOptionsFromFormData,
|
||||
getPoints,
|
||||
getLayer,
|
||||
} from './Geojson';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
@@ -295,3 +300,158 @@ test('DeckGLGeoJson falls back to legacy map_style when provider-specific style
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const baseFormData: SqlaFormData = {
|
||||
datasource: 'test_datasource',
|
||||
viz_type: 'deck_geojson',
|
||||
slice_id: 1,
|
||||
fill_color_picker: { r: 0, g: 0, b: 255, a: 1 },
|
||||
stroke_color_picker: { r: 0, g: 0, b: 0, a: 1 },
|
||||
};
|
||||
|
||||
const baseLayerArgs = {
|
||||
onContextMenu: jest.fn(),
|
||||
filterState: undefined,
|
||||
setDataMask: jest.fn(),
|
||||
payload: { data: { type: 'FeatureCollection', features: [] } },
|
||||
setTooltip: jest.fn(),
|
||||
emitCrossFilters: false,
|
||||
};
|
||||
|
||||
test('getLayer preserves rendering for existing charts without new point radius fields', () => {
|
||||
// Simulate form data from an existing chart that only has point_radius_scale
|
||||
const legacyFormData = {
|
||||
...baseFormData,
|
||||
point_radius_scale: 200,
|
||||
// point_radius and point_radius_units intentionally absent
|
||||
};
|
||||
|
||||
const layer = getLayer({ formData: legacyFormData, ...baseLayerArgs });
|
||||
const { props } = layer;
|
||||
|
||||
// Should match deck.gl defaults, NOT the new control panel defaults
|
||||
expect(props.getPointRadius).toBe(1); // deck.gl default, not 10
|
||||
expect(props.pointRadiusUnits).toBe('meters'); // deck.gl default, not 'pixels'
|
||||
expect(props.pointRadiusScale).toBe(200); // user's saved value preserved
|
||||
});
|
||||
|
||||
test('getLayer uses control panel defaults for new charts', () => {
|
||||
const newChartFormData = {
|
||||
...baseFormData,
|
||||
point_radius: 10,
|
||||
point_radius_units: 'pixels',
|
||||
point_radius_scale: 1,
|
||||
};
|
||||
|
||||
const layer = getLayer({ formData: newChartFormData, ...baseLayerArgs });
|
||||
const { props } = layer;
|
||||
|
||||
expect(props.getPointRadius).toBe(10);
|
||||
expect(props.pointRadiusUnits).toBe('pixels');
|
||||
expect(props.pointRadiusScale).toBe(1);
|
||||
});
|
||||
|
||||
test('getLayer falls back to defaults when legacy fields are null', () => {
|
||||
// The old point_radius_scale control had `default: null`, so legacy charts
|
||||
// can have null persisted; it must fall back to 1, not coerce to 0.
|
||||
const nullFormData = {
|
||||
...baseFormData,
|
||||
point_radius: null,
|
||||
point_radius_scale: null,
|
||||
};
|
||||
|
||||
const layer = getLayer({ formData: nullFormData, ...baseLayerArgs });
|
||||
const { props } = layer;
|
||||
|
||||
expect(props.getPointRadius).toBe(1);
|
||||
expect(props.pointRadiusScale).toBe(1);
|
||||
});
|
||||
|
||||
test('getLayer preserves an explicit zero radius scale', () => {
|
||||
const zeroFormData = {
|
||||
...baseFormData,
|
||||
point_radius_scale: 0,
|
||||
};
|
||||
|
||||
const layer = getLayer({ formData: zeroFormData, ...baseLayerArgs });
|
||||
const { props } = layer;
|
||||
|
||||
expect(props.pointRadiusScale).toBe(0);
|
||||
});
|
||||
|
||||
test('getLayer coerces free-form string radius values to numbers', () => {
|
||||
// Free-form SelectControls can store user-typed values as strings
|
||||
const stringFormData = {
|
||||
...baseFormData,
|
||||
point_radius: '3',
|
||||
point_radius_scale: '0.25',
|
||||
};
|
||||
|
||||
const layer = getLayer({ formData: stringFormData, ...baseLayerArgs });
|
||||
const { props } = layer;
|
||||
|
||||
expect(props.getPointRadius).toBe(3);
|
||||
expect(props.pointRadiusScale).toBe(0.25);
|
||||
});
|
||||
|
||||
type ControlConfig = {
|
||||
default?: unknown;
|
||||
validators?: unknown[];
|
||||
choices?: [unknown, unknown][];
|
||||
renderTrigger?: boolean;
|
||||
};
|
||||
|
||||
const controlItems = controlPanel.controlPanelSections
|
||||
.filter(
|
||||
(s: ControlPanelSectionConfig | null): s is ControlPanelSectionConfig =>
|
||||
s !== null,
|
||||
)
|
||||
.flatMap((section: ControlPanelSectionConfig) => section.controlSetRows)
|
||||
.flat();
|
||||
|
||||
const findControlConfig = (name: string): ControlConfig | undefined =>
|
||||
(controlItems.filter(isCustomControlItem) as CustomControlItem[]).find(
|
||||
(item: CustomControlItem) => item.name === name,
|
||||
)?.config as ControlConfig | undefined;
|
||||
|
||||
test('controlPanel exposes a Point Radius control defaulting to 10', () => {
|
||||
const config = findControlConfig('point_radius');
|
||||
expect(config).toBeDefined();
|
||||
expect(config?.default).toBe(10);
|
||||
expect(config?.renderTrigger).toBe(true);
|
||||
expect(config?.validators).toHaveLength(1);
|
||||
expect(config?.choices).toEqual(
|
||||
expect.arrayContaining([
|
||||
[1, '1'],
|
||||
[10, '10'],
|
||||
[100, '100'],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('controlPanel Point Radius Scale defaults to 1 with fractional choices', () => {
|
||||
const config = findControlConfig('point_radius_scale');
|
||||
expect(config).toBeDefined();
|
||||
expect(config?.default).toBe(1);
|
||||
expect(config?.renderTrigger).toBe(true);
|
||||
expect(config?.validators).toHaveLength(1);
|
||||
expect(config?.choices).toEqual(
|
||||
expect.arrayContaining([
|
||||
[0.1, '0.1'],
|
||||
[1, '1'],
|
||||
[10, '10'],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('controlPanel Point Radius Units defaults to pixels', () => {
|
||||
const config = findControlConfig('point_radius_units');
|
||||
expect(config).toBeDefined();
|
||||
expect(config?.default).toBe('pixels');
|
||||
expect(config?.renderTrigger).toBe(true);
|
||||
expect(config?.choices?.map(([value]) => value)).toEqual([
|
||||
'pixels',
|
||||
'meters',
|
||||
'common',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -254,6 +254,15 @@ export const computeGeoJsonIconOptionsFromFormData = (
|
||||
iconSizeUnits: fd.icon_size_unit,
|
||||
});
|
||||
|
||||
// Free-form SelectControls can yield string values, and legacy charts may have
|
||||
// null persisted for these fields, so coerce to a number (falling back to the
|
||||
// provided default for null/undefined/NaN input, while preserving an explicit 0)
|
||||
// before handing them to deck.gl's numeric layer props.
|
||||
const toNumber = (value: unknown, fallback: number) => {
|
||||
const num = Number(value ?? fallback);
|
||||
return Number.isFinite(num) ? num : fallback;
|
||||
};
|
||||
|
||||
export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||
formData,
|
||||
onContextMenu,
|
||||
@@ -328,7 +337,11 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||
getFillColor(feature, filterState?.value),
|
||||
getLineColor,
|
||||
getLineWidth: fd.line_width || 1,
|
||||
pointRadiusScale: fd.point_radius_scale,
|
||||
// Use deck.gl defaults as fallbacks for backward compatibility with existing charts.
|
||||
// New charts will get control panel defaults (point_radius=10, units='pixels', scale=1).
|
||||
getPointRadius: toNumber(fd.point_radius, 1),
|
||||
pointRadiusUnits: fd.point_radius_units ?? 'meters',
|
||||
pointRadiusScale: toNumber(fd.point_radius_scale, 1),
|
||||
lineWidthUnits: fd.line_width_unit,
|
||||
pointType,
|
||||
...labelOpts,
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
legacyValidateInteger,
|
||||
isFeatureEnabled,
|
||||
FeatureFlag,
|
||||
validateNumber,
|
||||
validateInteger,
|
||||
} from '@superset-ui/core';
|
||||
import { formatSelectOptions } from '../../utilities/utils';
|
||||
import {
|
||||
@@ -352,15 +354,56 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'point_radius',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Point Radius'),
|
||||
description: t(
|
||||
'The radius of point features, in the units specified below. ' +
|
||||
'The final rendered size is this value multiplied by Point Radius Scale.',
|
||||
),
|
||||
validators: [validateInteger],
|
||||
default: 10,
|
||||
choices: formatSelectOptions([1, 5, 10, 20, 50, 100]),
|
||||
renderTrigger: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'point_radius_scale',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Point Radius Scale'),
|
||||
validators: [legacyValidateInteger],
|
||||
default: null,
|
||||
choices: formatSelectOptions([0, 100, 200, 300, 500]),
|
||||
description: t(
|
||||
'A multiplier applied to the point radius. ' +
|
||||
'Use this to uniformly scale all points.',
|
||||
),
|
||||
validators: [validateNumber],
|
||||
default: 1,
|
||||
choices: formatSelectOptions([0.1, 0.5, 1, 2, 5, 10]),
|
||||
renderTrigger: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'point_radius_units',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Point Radius Units'),
|
||||
description: t(
|
||||
'The unit for point radius. Use "pixels" for consistent ' +
|
||||
'screen-space sizing regardless of zoom level.',
|
||||
),
|
||||
default: 'pixels',
|
||||
choices: [
|
||||
['pixels', t('Pixels')],
|
||||
['meters', t('Meters')],
|
||||
['common', t('Common (unit per pixel at zoom 0)')],
|
||||
],
|
||||
renderTrigger: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -295,6 +295,7 @@ export interface ListViewProps<T extends object = any> {
|
||||
name: ReactNode;
|
||||
onSelect: (rows: any[]) => any;
|
||||
type?: 'primary' | 'secondary' | 'danger';
|
||||
hidden?: (rows: any[]) => boolean;
|
||||
}>;
|
||||
bulkSelectEnabled?: boolean;
|
||||
disableBulkSelect?: () => void;
|
||||
@@ -509,7 +510,16 @@ export function ListView<T extends object = any>({
|
||||
{t('Deselect all')}
|
||||
</span>
|
||||
<div className="divider" />
|
||||
{bulkActions.map(action => (
|
||||
{bulkActions
|
||||
.filter(
|
||||
action =>
|
||||
!action.hidden?.(
|
||||
selectedFlatRows.map(
|
||||
(r: any) => r.original,
|
||||
),
|
||||
),
|
||||
)
|
||||
.map(action => (
|
||||
<Button
|
||||
data-test="bulk-select-action"
|
||||
data-test-action-key={action.key}
|
||||
|
||||
@@ -299,7 +299,10 @@ test('checkIsApplyDisabled returns true when required filter is missing value in
|
||||
);
|
||||
});
|
||||
|
||||
test('checkIsApplyDisabled handles filter count mismatch', () => {
|
||||
test('checkIsApplyDisabled enables Apply when Selected has a filter value not yet in Applied', () => {
|
||||
// Regression: when a required filter's default isn't applied (Applied missing
|
||||
// the entry) and the user types a value, Selected gains an entry Applied
|
||||
// doesn't have. Apply must be enabled so the user can commit the value.
|
||||
const dataMaskSelected: DataMaskStateWithId = {
|
||||
'filter-1': {
|
||||
id: 'filter-1',
|
||||
@@ -322,7 +325,7 @@ test('checkIsApplyDisabled handles filter count mismatch', () => {
|
||||
const filters = [createFilter('filter-1'), createFilter('filter-2')];
|
||||
|
||||
expect(checkIsApplyDisabled(dataMaskSelected, dataMaskApplied, filters)).toBe(
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -74,13 +74,9 @@ export const checkIsApplyDisabled = (
|
||||
const selectedExtraFormData = getOnlyExtraFormData(dataMaskSelected);
|
||||
const appliedExtraFormData = getOnlyExtraFormData(dataMaskApplied);
|
||||
|
||||
// Check counts first
|
||||
const selectedCount = Object.keys(selectedExtraFormData).length;
|
||||
const appliedCount = Object.keys(appliedExtraFormData).length;
|
||||
|
||||
if (selectedCount !== appliedCount) return true;
|
||||
|
||||
// Check for changes
|
||||
// Check for changes. ignoreUndefined drops empty keys on both sides so that
|
||||
// a filter present in Selected with a real value but absent (or undefined)
|
||||
// in Applied is correctly detected as a change.
|
||||
const dataEqual = areObjectsEqual(
|
||||
selectedExtraFormData,
|
||||
appliedExtraFormData,
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,9 @@ const isUserDashboardOwner = (
|
||||
user: UserWithPermissionsAndRoles | UndefinedUser,
|
||||
) =>
|
||||
isUserWithPermissionsAndRoles(user) &&
|
||||
dashboard.owners.some(owner => owner.id === user.userId);
|
||||
[...dashboard.owners, ...(dashboard.extra_owners ?? [])].some(
|
||||
owner => owner.id === user.userId,
|
||||
);
|
||||
|
||||
export const canUserEditDashboard = (
|
||||
dashboard: Dashboard,
|
||||
|
||||
@@ -121,6 +121,38 @@ test('when user edits a filter without changing targets, their selection is pres
|
||||
);
|
||||
});
|
||||
|
||||
test('when a required range filter was cleared to [null, null], modifying it applies the new default instead of the cleared state', () => {
|
||||
// Regression for the PR #40470 review: [null, null] is a range filter's
|
||||
// canonical "cleared" value. It must count as "no value" so the empty state
|
||||
// does not wipe a newly-defined default — consistent with `loadedHasValue`
|
||||
// in fillNativeFilters.
|
||||
const initialState: DataMaskStateWithId = {
|
||||
'NATIVE_FILTER-1': {
|
||||
id: 'NATIVE_FILTER-1',
|
||||
...getInitialDataMask('NATIVE_FILTER-1'),
|
||||
filterState: { value: [null, null] },
|
||||
},
|
||||
};
|
||||
|
||||
const oldFilters = {
|
||||
'NATIVE_FILTER-1': createFilter('NATIVE_FILTER-1', 'col_a', {
|
||||
enableEmptyFilter: true,
|
||||
}),
|
||||
};
|
||||
|
||||
const modifiedFilter: Filter = {
|
||||
...createFilter('NATIVE_FILTER-1', 'col_a', { enableEmptyFilter: true }),
|
||||
defaultDataMask: { filterState: { value: [10, 20] } },
|
||||
};
|
||||
|
||||
const action = createModifyAction(modifiedFilter, oldFilters);
|
||||
|
||||
const result = reducer(initialState, action);
|
||||
|
||||
// The cleared [null, null] state must not be preserved; the new default wins.
|
||||
expect(result['NATIVE_FILTER-1']?.filterState?.value).toEqual([10, 20]);
|
||||
});
|
||||
|
||||
// Runtime data from the server can contain null entries in
|
||||
// chart_customization_config even though the TS type does not include | null
|
||||
// yet. These helpers build HYDRATE_DASHBOARD actions that mirror that reality.
|
||||
|
||||
@@ -109,10 +109,50 @@ function fillNativeFilters(
|
||||
) {
|
||||
filterConfig.forEach((filter: Filter) => {
|
||||
const dataMask = initialDataMask || {};
|
||||
const loaded = dataMask[filter.id];
|
||||
|
||||
// The shallow spread of `loaded` would override `filter.defaultDataMask`
|
||||
// even when the loaded mask is incomplete (e.g. a permalink captured
|
||||
// mid-initialization), wiping out a valid default. For REQUIRED filters
|
||||
// with a default, fall back to the default when the loaded mask carries
|
||||
// no real value OR is missing the extraFormData needed to filter charts.
|
||||
// Non-required filters keep current behavior so a user's explicit clear
|
||||
// isn't undone.
|
||||
const isRequired = !!filter.controlValues?.enableEmptyFilter;
|
||||
const loadedValue = loaded?.filterState?.value;
|
||||
const loadedHasValue =
|
||||
loadedValue !== undefined &&
|
||||
loadedValue !== null &&
|
||||
!(
|
||||
// Treat all-null arrays (range filters use [null, null] as their
|
||||
// canonical cleared value) and empty arrays as "no value".
|
||||
(Array.isArray(loadedValue) && loadedValue.every(v => v === null))
|
||||
);
|
||||
const loadedHasExtraFormData =
|
||||
!!loaded?.extraFormData && Object.keys(loaded.extraFormData).length > 0;
|
||||
const defaultHasExtraFormData =
|
||||
!!filter.defaultDataMask?.extraFormData &&
|
||||
Object.keys(filter.defaultDataMask.extraFormData).length > 0;
|
||||
// Restore when:
|
||||
// (1) loaded value is empty — classic "default wiped by stale permalink", OR
|
||||
// (2) loaded has a value but no extraFormData and the default does — the
|
||||
// "value present in UI but not applied to charts" gap-window case where
|
||||
// a permalink was captured before FilterValue produced extraFormData.
|
||||
const shouldRestoreDefault =
|
||||
isRequired &&
|
||||
!!filter.defaultDataMask &&
|
||||
(!loadedHasValue || (!loadedHasExtraFormData && defaultHasExtraFormData));
|
||||
|
||||
mergedDataMask[filter.id] = {
|
||||
...getInitialDataMask(filter.id), // take initial data
|
||||
...filter.defaultDataMask, // if something new came from BE - take it
|
||||
...dataMask[filter.id],
|
||||
...loaded,
|
||||
...(shouldRestoreDefault
|
||||
? {
|
||||
filterState: filter.defaultDataMask?.filterState,
|
||||
extraFormData: filter.defaultDataMask?.extraFormData,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
if (
|
||||
currentFilters &&
|
||||
@@ -155,12 +195,28 @@ function updateDataMaskForFilterChanges(
|
||||
// Check if targets are equal
|
||||
const areTargetsEqual = isEqual(prevFilterDef?.targets, filter?.targets);
|
||||
|
||||
// Preserve state only if filter exists, has enableEmptyFilter=true and targets match
|
||||
// For required filters, only preserve existing state when it actually has
|
||||
// a value — otherwise the empty existing state would wipe the (possibly
|
||||
// newly-defined) default. defaultToFirstItem filters keep the old behavior:
|
||||
// FilterValue re-resolves the first item at runtime, so preserving the
|
||||
// mid-init empty state is fine.
|
||||
const isRequired = !!filter.controlValues?.enableEmptyFilter;
|
||||
const isFirstItem = !!filter.controlValues?.defaultToFirstItem;
|
||||
const existingValue = existingFilter?.filterState?.value;
|
||||
const hasExistingValue =
|
||||
existingValue !== undefined &&
|
||||
existingValue !== null &&
|
||||
// Treat all-null arrays (range filters use [null, null] as their
|
||||
// canonical cleared value) and empty arrays as "no value", consistent
|
||||
// with `loadedHasValue` in fillNativeFilters above. `[].every()` is
|
||||
// true, so this also covers the empty-array case.
|
||||
!(Array.isArray(existingValue) && existingValue.every(v => v === null));
|
||||
|
||||
const shouldPreserveState =
|
||||
existingFilter &&
|
||||
areTargetsEqual &&
|
||||
(filter.controlValues?.enableEmptyFilter ||
|
||||
filter.controlValues?.defaultToFirstItem);
|
||||
(isRequired || isFirstItem) &&
|
||||
(isFirstItem || hasExistingValue);
|
||||
|
||||
mergedDataMask[filter.id] = {
|
||||
...getInitialDataMask(filter.id),
|
||||
|
||||
@@ -177,9 +177,11 @@ export const hydrateExplore =
|
||||
can_copy_clipboard: granularExport
|
||||
? findPermission('can_copy_clipboard', 'Superset', user?.roles)
|
||||
: findPermission('can_csv', 'Superset', user?.roles),
|
||||
can_overwrite: ensureIsArray(slice?.owners).includes(
|
||||
user?.userId as number,
|
||||
),
|
||||
can_overwrite:
|
||||
ensureIsArray(slice?.owners).includes(user?.userId as number) ||
|
||||
ensureIsArray(metadata?.extra_owners).some(
|
||||
(o: { id: number }) => o.id === user?.userId,
|
||||
),
|
||||
isDatasourceMetaLoading: false,
|
||||
isStarred: false,
|
||||
triggerRender: false,
|
||||
|
||||
@@ -75,6 +75,7 @@ interface SaveModalProps extends RouteComponentProps {
|
||||
alert?: string;
|
||||
sliceName?: string;
|
||||
slice?: Record<string, any>;
|
||||
can_overwrite?: boolean;
|
||||
datasource?: Record<string, any>;
|
||||
dashboardId: '' | number | null;
|
||||
isVisible: boolean;
|
||||
@@ -128,7 +129,8 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
|
||||
|
||||
canOverwriteSlice(): boolean {
|
||||
return (
|
||||
(isUserAdmin(this.props.user) ||
|
||||
(this.props.can_overwrite ||
|
||||
isUserAdmin(this.props.user) ||
|
||||
this.props.slice?.owners?.includes(this.props.user.userId)) &&
|
||||
!this.props.slice?.is_managed_externally
|
||||
);
|
||||
@@ -819,6 +821,7 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
|
||||
interface StateProps {
|
||||
datasource: any;
|
||||
slice: any;
|
||||
can_overwrite: boolean;
|
||||
user: UserWithPermissionsAndRoles;
|
||||
dashboards: any;
|
||||
alert: any;
|
||||
@@ -833,6 +836,7 @@ function mapStateToProps({
|
||||
return {
|
||||
datasource: explore.datasource,
|
||||
slice: explore.slice,
|
||||
can_overwrite: explore.can_overwrite,
|
||||
user,
|
||||
dashboards: saveModal.dashboards,
|
||||
alert: saveModal.saveModalAlert,
|
||||
|
||||
@@ -45,6 +45,7 @@ import TextControl from 'src/explore/components/controls/TextControl';
|
||||
import CheckboxControl from 'src/explore/components/controls/CheckboxControl';
|
||||
import PopoverSection from '@superset-ui/core/components/PopoverSection';
|
||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||
import { ensureAppRoot } from 'src/utils/pathUtils';
|
||||
import {
|
||||
ANNOTATION_SOURCE_TYPES,
|
||||
ANNOTATION_TYPES,
|
||||
@@ -145,7 +146,7 @@ const NotFoundContent = () => (
|
||||
<span>
|
||||
{t('Add an annotation layer')}{' '}
|
||||
<a
|
||||
href="/annotationlayer/list"
|
||||
href={ensureAppRoot('/annotationlayer/list')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -112,7 +112,9 @@ class AdhocFilterPopoverTrigger extends PureComponent<
|
||||
onOpenChange={togglePopover}
|
||||
destroyOnHidden
|
||||
>
|
||||
{this.props.children}
|
||||
{/* Wrap in span so the Popover can attach a ref without relying
|
||||
on findDOMNode (deprecated in React 18+). */}
|
||||
<span>{this.props.children}</span>
|
||||
</ControlPopover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,10 +64,7 @@ interface FixedOrMetricControlState {
|
||||
metricValue: MetricValue | null;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
default: { type: controlTypes.fixed, value: 5 },
|
||||
};
|
||||
const DEFAULT_VALUE: ControlValue = { type: controlTypes.fixed, value: 5 };
|
||||
|
||||
export default class FixedOrMetricControl extends Component<
|
||||
FixedOrMetricControlProps,
|
||||
@@ -79,10 +76,11 @@ export default class FixedOrMetricControl extends Component<
|
||||
this.setType = this.setType.bind(this);
|
||||
this.setFixedValue = this.setFixedValue.bind(this);
|
||||
this.setMetric = this.setMetric.bind(this);
|
||||
const defaultValue = props.default ?? DEFAULT_VALUE;
|
||||
const type = (props.value?.type ??
|
||||
props.default?.type ??
|
||||
defaultValue.type ??
|
||||
controlTypes.fixed) as 'fix' | 'metric';
|
||||
const rawValue = props.value?.value ?? props.default?.value ?? '100';
|
||||
const rawValue = props.value?.value ?? defaultValue.value ?? '100';
|
||||
const fixedValue =
|
||||
type === controlTypes.fixed && typeof rawValue !== 'object'
|
||||
? rawValue
|
||||
@@ -121,7 +119,7 @@ export default class FixedOrMetricControl extends Component<
|
||||
}
|
||||
|
||||
render() {
|
||||
const value = this.props.value ?? this.props.default;
|
||||
const value = this.props.value ?? this.props.default ?? DEFAULT_VALUE;
|
||||
const type = value?.type ?? controlTypes.fixed;
|
||||
const columns = this.props.datasource
|
||||
? this.props.datasource.columns
|
||||
@@ -203,6 +201,3 @@ export default class FixedOrMetricControl extends Component<
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error - defaultProps for backward compatibility
|
||||
FixedOrMetricControl.defaultProps = defaultProps;
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
Tooltip,
|
||||
} from '@superset-ui/core/components';
|
||||
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
||||
import { noOp } from 'src/utils/common';
|
||||
import {
|
||||
AGGREGATES_OPTIONS,
|
||||
POPOVER_INITIAL_HEIGHT,
|
||||
@@ -106,12 +105,6 @@ interface AdhocMetricEditPopoverState {
|
||||
height: number;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
columns: [],
|
||||
getCurrentTab: noOp,
|
||||
isNewMetric: false,
|
||||
};
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
.metric-option {
|
||||
& > svg {
|
||||
@@ -199,8 +192,12 @@ class AdhocMetricEditPopover extends PureComponent<
|
||||
}
|
||||
|
||||
getDefaultTab() {
|
||||
const { adhocMetric, savedMetric, savedMetricsOptions, isNewMetric } =
|
||||
this.props;
|
||||
const {
|
||||
adhocMetric,
|
||||
savedMetric,
|
||||
savedMetricsOptions,
|
||||
isNewMetric = false,
|
||||
} = this.props;
|
||||
if (isDefined(adhocMetric.column) || isDefined(adhocMetric.sqlExpression)) {
|
||||
return adhocMetric.expressionType;
|
||||
}
|
||||
@@ -353,7 +350,7 @@ class AdhocMetricEditPopover extends PureComponent<
|
||||
onClose,
|
||||
onResize,
|
||||
datasource,
|
||||
isNewMetric,
|
||||
isNewMetric = false,
|
||||
isLabelModified,
|
||||
...popoverProps
|
||||
} = this.props;
|
||||
@@ -606,8 +603,6 @@ class AdhocMetricEditPopover extends PureComponent<
|
||||
);
|
||||
}
|
||||
}
|
||||
// @ts-expect-error - defaultProps for backward compatibility
|
||||
AdhocMetricEditPopover.defaultProps = defaultProps;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Thin functional wrapper that injects compatibility data from Redux.
|
||||
|
||||
@@ -274,7 +274,9 @@ class AdhocMetricPopoverTrigger extends PureComponent<
|
||||
title={popoverTitle}
|
||||
destroyOnHidden
|
||||
>
|
||||
{this.props.children}
|
||||
{/* Wrap in span so the Popover can attach a ref without relying
|
||||
on findDOMNode (deprecated in React 18+). */}
|
||||
<span>{this.props.children}</span>
|
||||
</ControlPopover>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -32,13 +32,6 @@ import MetricDefinitionValue from './MetricDefinitionValue';
|
||||
import AdhocMetric from './AdhocMetric';
|
||||
import AdhocMetricPopoverTrigger from './AdhocMetricPopoverTrigger';
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
clearable: true,
|
||||
savedMetrics: [],
|
||||
columns: [],
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function getOptionsForSavedMetrics(
|
||||
savedMetrics: any,
|
||||
@@ -122,11 +115,11 @@ export interface MetricsControlProps {
|
||||
}
|
||||
|
||||
const MetricsControl = ({
|
||||
onChange,
|
||||
onChange = () => {},
|
||||
multi,
|
||||
value: propsValue,
|
||||
columns,
|
||||
savedMetrics,
|
||||
columns = [],
|
||||
savedMetrics = [],
|
||||
datasource,
|
||||
...props
|
||||
}: MetricsControlProps) => {
|
||||
@@ -351,6 +344,4 @@ const MetricsControl = ({
|
||||
);
|
||||
};
|
||||
|
||||
MetricsControl.defaultProps = defaultProps;
|
||||
|
||||
export default MetricsControl;
|
||||
|
||||
@@ -88,6 +88,7 @@ export interface ExplorePageInitialData {
|
||||
created_on_humanized: string;
|
||||
changed_on_humanized: string;
|
||||
owners: string[];
|
||||
extra_owners?: { id: number; first_name: string; last_name: string }[];
|
||||
created_by?: string;
|
||||
changed_by?: string;
|
||||
color_namespace?: string;
|
||||
|
||||
@@ -18,13 +18,9 @@
|
||||
*/
|
||||
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
JsonResponse,
|
||||
SupersetClient,
|
||||
isFeatureEnabled,
|
||||
} from '@superset-ui/core';
|
||||
import { isFeatureEnabled } from '@superset-ui/core';
|
||||
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
|
||||
import DashboardCard from './DashboardCard';
|
||||
|
||||
@@ -35,7 +31,7 @@ const mockDashboard = {
|
||||
certification_details: 'Certified on 2022-01-01',
|
||||
published: true,
|
||||
url: '/dashboard/1',
|
||||
thumbnail_url: '/thumbnails/1.png',
|
||||
changed_on_utc: '2024-01-01T00:00:00',
|
||||
changed_on_delta_humanized: '2 days ago',
|
||||
owners: [
|
||||
{ id: 1, name: 'Alice', first_name: 'Alice', last_name: 'Doe' },
|
||||
@@ -104,57 +100,81 @@ test('Renders the modified date', () => {
|
||||
expect(modifiedDateElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should fetch thumbnail when dashboard has no thumbnail URL and feature flag is enabled', async () => {
|
||||
const mockGet = jest.spyOn(SupersetClient, 'get').mockResolvedValue({
|
||||
json: { result: { thumbnail_url: '/new-thumbnail.png' } },
|
||||
} as unknown as JsonResponse);
|
||||
describe('thumbnail URL construction', () => {
|
||||
let fetchSpy: jest.SpyInstance;
|
||||
|
||||
const { rerender } = render(
|
||||
<DashboardCard
|
||||
dashboard={{
|
||||
id: 1,
|
||||
thumbnail_url: '',
|
||||
changed_by_name: '',
|
||||
changed_by: '',
|
||||
dashboard_title: '',
|
||||
published: false,
|
||||
url: '',
|
||||
owners: [],
|
||||
}}
|
||||
hasPerm={() => true}
|
||||
bulkSelectEnabled={false}
|
||||
loading={false}
|
||||
saveFavoriteStatus={() => {}}
|
||||
favoriteStatus={false}
|
||||
handleBulkDashboardExport={() => {}}
|
||||
onDelete={() => {}}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockGet).toHaveBeenCalledWith({
|
||||
endpoint: '/api/v1/dashboard/1',
|
||||
});
|
||||
beforeEach(() => {
|
||||
fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
|
||||
blob: () => Promise.resolve(new Blob([''], { type: 'image/png' })),
|
||||
} as Response);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
const renderCard = (dashboard: object) =>
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardCard
|
||||
dashboard={dashboard as any}
|
||||
hasPerm={() => true}
|
||||
bulkSelectEnabled={false}
|
||||
loading={false}
|
||||
showThumbnails
|
||||
saveFavoriteStatus={() => {}}
|
||||
favoriteStatus={false}
|
||||
handleBulkDashboardExport={() => {}}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
test('constructs thumbnail URL from dashboard id and changed_on_utc', () => {
|
||||
renderCard({
|
||||
id: 2,
|
||||
changed_by_name: '',
|
||||
changed_by: '',
|
||||
dashboard_title: 'UTC Dashboard',
|
||||
published: false,
|
||||
url: '/dashboard/2',
|
||||
owners: [],
|
||||
changed_on_utc: '2024-01-01T00:00:00',
|
||||
});
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
'/api/v1/dashboard/2/thumbnail/2024-01-01T00%3A00%3A00/',
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to changed_on when changed_on_utc is absent', () => {
|
||||
renderCard({
|
||||
id: 3,
|
||||
changed_by_name: '',
|
||||
changed_by: '',
|
||||
dashboard_title: 'Fallback Dashboard',
|
||||
published: false,
|
||||
url: '/dashboard/3',
|
||||
owners: [],
|
||||
changed_on: '2024-06-01T12:00:00',
|
||||
});
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
'/api/v1/dashboard/3/thumbnail/2024-06-01T12%3A00%3A00/',
|
||||
);
|
||||
});
|
||||
|
||||
test('renders no thumbnail when both changed_on_utc and changed_on are absent', () => {
|
||||
renderCard({
|
||||
id: 4,
|
||||
changed_by_name: '',
|
||||
changed_by: '',
|
||||
dashboard_title: 'No Timestamp Dashboard',
|
||||
published: false,
|
||||
url: '/dashboard/4',
|
||||
owners: [],
|
||||
});
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
rerender(
|
||||
<DashboardCard
|
||||
dashboard={{
|
||||
id: 1,
|
||||
thumbnail_url: '/new-thumbnail.png',
|
||||
changed_by_name: '',
|
||||
changed_by: '',
|
||||
dashboard_title: '',
|
||||
published: false,
|
||||
url: '',
|
||||
owners: [],
|
||||
}}
|
||||
hasPerm={() => true}
|
||||
bulkSelectEnabled={false}
|
||||
loading={false}
|
||||
saveFavoriteStatus={() => {}}
|
||||
favoriteStatus={false}
|
||||
handleBulkDashboardExport={() => {}}
|
||||
onDelete={() => {}}
|
||||
/>,
|
||||
);
|
||||
mockGet.mockRestore();
|
||||
});
|
||||
|
||||
@@ -16,14 +16,9 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
isFeatureEnabled,
|
||||
FeatureFlag,
|
||||
SupersetClient,
|
||||
} from '@superset-ui/core';
|
||||
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
|
||||
import { CardStyles } from 'src/views/CRUD/utils';
|
||||
import {
|
||||
Dropdown,
|
||||
@@ -69,33 +64,11 @@ function DashboardCard({
|
||||
const canEdit = hasPerm('can_write');
|
||||
const canDelete = hasPerm('can_write');
|
||||
const canExport = hasPerm('can_export');
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
|
||||
const [fetchingThumbnail, setFetchingThumbnail] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
// fetch thumbnail only if it's not already fetched
|
||||
if (
|
||||
!fetchingThumbnail &&
|
||||
dashboard.id &&
|
||||
(thumbnailUrl === undefined || thumbnailUrl === null) &&
|
||||
isFeatureEnabled(FeatureFlag.Thumbnails)
|
||||
) {
|
||||
// fetch thumbnail
|
||||
if (dashboard.thumbnail_url) {
|
||||
// set to empty string if null so that we don't
|
||||
// keep fetching the thumbnail
|
||||
setThumbnailUrl(dashboard.thumbnail_url || '');
|
||||
return;
|
||||
}
|
||||
setFetchingThumbnail(true);
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/dashboard/${dashboard.id}`,
|
||||
}).then(({ json = {} }) => {
|
||||
setThumbnailUrl(json.result?.thumbnail_url || '');
|
||||
setFetchingThumbnail(false);
|
||||
});
|
||||
}
|
||||
}, [dashboard, thumbnailUrl]);
|
||||
const digest = dashboard.changed_on_utc || dashboard.changed_on;
|
||||
const thumbnailUrl =
|
||||
isFeatureEnabled(FeatureFlag.Thumbnails) && dashboard.id && digest
|
||||
? `/api/v1/dashboard/${dashboard.id}/thumbnail/${encodeURIComponent(digest)}/`
|
||||
: '';
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ import {
|
||||
DatasetObject,
|
||||
} from 'src/features/datasets/AddDataset/types';
|
||||
import { Table } from 'src/hooks/apiResources';
|
||||
import { Typography } from '@superset-ui/core/components/Typography';
|
||||
import { ensureAppRoot } from 'src/utils/pathUtils';
|
||||
import { Typography } from '@superset-ui/core/components/Typography';
|
||||
|
||||
interface LeftPanelProps {
|
||||
setDataset: Dispatch<SetStateAction<object>>;
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { VizType } from '@superset-ui/core';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { act } from 'react';
|
||||
import handleResourceExport from 'src/utils/export';
|
||||
import { LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||
import ChartTable from './ChartTable';
|
||||
|
||||
@@ -21,7 +21,7 @@ import { styled, css, useTheme } from '@apache-superset/core/theme';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ensureStaticPrefix } from 'src/utils/assetUrl';
|
||||
import { ensureAppRoot } from 'src/utils/pathUtils';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { getUrlParam, isUrlExternal } from 'src/utils/urlUtils';
|
||||
import { MainNav, MenuItem } from '@superset-ui/core/components/Menu';
|
||||
import { Tooltip, Grid, Row, Col, Image } from '@superset-ui/core/components';
|
||||
import { GenericLink } from 'src/components';
|
||||
@@ -153,7 +153,7 @@ const StyledBrandWrapper = styled.div<{ margin?: string }>`
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledBrandLink = styled(Typography.Link)`
|
||||
const StyledBrandLink = styled(GenericLink)`
|
||||
${({ theme }) => css`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -218,10 +218,10 @@ export function Menu({
|
||||
const path = location.pathname;
|
||||
switch (true) {
|
||||
case path.startsWith(Paths.Dashboard):
|
||||
setActiveTabs(['Dashboards']);
|
||||
setActiveTabs([t('Dashboards')]);
|
||||
break;
|
||||
case path.startsWith(Paths.Chart) || path.startsWith(Paths.Explore):
|
||||
setActiveTabs(['Charts']);
|
||||
setActiveTabs([t('Charts')]);
|
||||
break;
|
||||
case path.startsWith(Paths.Datasets):
|
||||
setActiveTabs([datasetsLabel()]);
|
||||
@@ -263,9 +263,10 @@ export function Menu({
|
||||
|
||||
const childItems: MenuItem[] = [];
|
||||
childs?.forEach((child: MenuObjectChildProps | string, index1: number) => {
|
||||
if (typeof child === 'string' && child === '-' && label !== 'Data') {
|
||||
if (typeof child === 'string' && child === '-' && label !== t('Data')) {
|
||||
childItems.push({ type: 'divider', key: `divider-${index1}` });
|
||||
} else if (typeof child !== 'string') {
|
||||
Object.assign(child, { label: t(child.label) });
|
||||
childItems.push({
|
||||
key: `${child.label}`,
|
||||
label: child.isFrontendRoute ? (
|
||||
@@ -292,16 +293,24 @@ export function Menu({
|
||||
const renderBrand = () => {
|
||||
let link;
|
||||
if (theme.brandLogoUrl) {
|
||||
const brandHref = ensureAppRoot(theme.brandLogoHref);
|
||||
const brandImage = (
|
||||
<StyledImage
|
||||
preview={false}
|
||||
src={ensureStaticPrefix(theme.brandLogoUrl)}
|
||||
alt={theme.brandLogoAlt || 'Apache Superset'}
|
||||
height={theme.brandLogoHeight}
|
||||
/>
|
||||
);
|
||||
link = (
|
||||
<StyledBrandWrapper margin={theme.brandLogoMargin}>
|
||||
<StyledBrandLink href={ensureAppRoot(theme.brandLogoHref)}>
|
||||
<StyledImage
|
||||
preview={false}
|
||||
src={ensureStaticPrefix(theme.brandLogoUrl)}
|
||||
alt={theme.brandLogoAlt || 'Apache Superset'}
|
||||
height={theme.brandLogoHeight}
|
||||
/>
|
||||
</StyledBrandLink>
|
||||
{isUrlExternal(brandHref) ? (
|
||||
<Typography.Link className="navbar-brand" href={brandHref}>
|
||||
{brandImage}
|
||||
</Typography.Link>
|
||||
) : (
|
||||
<StyledBrandLink to={brandHref}>{brandImage}</StyledBrandLink>
|
||||
)}
|
||||
</StyledBrandWrapper>
|
||||
);
|
||||
} else if (isFrontendRoute(window.location.pathname)) {
|
||||
@@ -366,6 +375,7 @@ export function Menu({
|
||||
items={menu.map(item => {
|
||||
const props = {
|
||||
...item,
|
||||
label: t(item.label),
|
||||
isFrontendRoute: isFrontendRoute(item.url),
|
||||
childs: item.childs?.map(c => {
|
||||
if (typeof c === 'string') {
|
||||
@@ -429,15 +439,16 @@ export default function MenuWrapper({ data, ...rest }: MenuProps) {
|
||||
// Apply any label override for this item (keyed by FAB internal name).
|
||||
...(item.name && labelOverrides[item.name]
|
||||
? { label: labelOverrides[item.name]() }
|
||||
: {}),
|
||||
: { label: t(item.label) }),
|
||||
};
|
||||
|
||||
// Filter childs
|
||||
if (item.childs) {
|
||||
item.childs.forEach((child: MenuObjectChildProps | string) => {
|
||||
if (typeof child === 'string') {
|
||||
children.push(child);
|
||||
children.push(t(child));
|
||||
} else if ((child as MenuObjectChildProps).label) {
|
||||
Object.assign(child, { label: t(child.label) });
|
||||
children.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -106,11 +106,11 @@ export interface Dashboard {
|
||||
changed_by_name: string;
|
||||
changed_on_delta_humanized: string;
|
||||
changed_by: string;
|
||||
changed_on?: string;
|
||||
dashboard_title: string;
|
||||
id: number;
|
||||
published: boolean;
|
||||
url: string;
|
||||
thumbnail_url: string;
|
||||
owners: Owner[];
|
||||
tags: TagType[];
|
||||
created_by: object;
|
||||
|
||||
@@ -111,12 +111,15 @@ const createController = (
|
||||
...options,
|
||||
});
|
||||
|
||||
// Shared console spies
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
// Shared console spies — re-installed in beforeEach so each test starts
|
||||
// with a fresh call count and a clean implementation.
|
||||
let consoleSpy: jest.SpyInstance;
|
||||
let consoleErrorSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
// Setup DOM environment
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface Dashboard {
|
||||
changed_on: string;
|
||||
charts: string[]; // just chart names, unfortunately...
|
||||
owners: Owner[];
|
||||
extra_owners?: Owner[];
|
||||
roles: Role[];
|
||||
theme?: {
|
||||
id: number;
|
||||
|
||||
@@ -110,6 +110,34 @@ describe('getBootstrapData and helpers', () => {
|
||||
expect(staticAssetsPrefix()).toEqual(expectedStaticPrefix);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['//evil.example.com', 'protocol-relative URL'],
|
||||
// eslint-disable-next-line no-script-url -- intentional unsafe value under test
|
||||
['javascript:alert(1)', 'javascript scheme'],
|
||||
['https://evil.example.com', 'absolute URL'],
|
||||
['/foo"><img src=x>', 'path with HTML meta-characters'],
|
||||
])(
|
||||
'should fall back to the default root when application_root is %s (%s)',
|
||||
async unsafeRoot => {
|
||||
const customData = {
|
||||
common: {
|
||||
application_root: unsafeRoot,
|
||||
static_assets_prefix: '/custom-static/',
|
||||
},
|
||||
};
|
||||
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify(customData)}'></div>`;
|
||||
|
||||
jest.resetModules();
|
||||
const { default: getBootstrapData, applicationRoot } =
|
||||
await import('./getBootstrapData');
|
||||
getBootstrapData();
|
||||
|
||||
const expectedAppRoot =
|
||||
DEFAULT_BOOTSTRAP_DATA.common.application_root.replace(/\/$/, '');
|
||||
expect(applicationRoot()).toEqual(expectedAppRoot);
|
||||
},
|
||||
);
|
||||
|
||||
test('should defaults without trailing slashes when #app element does not include application_root or static_assets_prefix', async () => {
|
||||
// Set up the fake #app element
|
||||
const customData = {
|
||||
|
||||
@@ -38,7 +38,34 @@ const normalizePathWithFallback = (
|
||||
fallback: string,
|
||||
): string => (path ?? fallback).replace(/\/$/, '');
|
||||
|
||||
const APPLICATION_ROOT_NO_TRAILING_SLASH = normalizePathWithFallback(
|
||||
/**
|
||||
* Matches a plain absolute path prefix (e.g. "" for root deployments or
|
||||
* "/analytics" for a subdirectory). The character after the leading slash must
|
||||
* not be another slash, so protocol-relative URLs ("//host") and scheme-bearing
|
||||
* values ("javascript:...") do not qualify.
|
||||
*/
|
||||
const SAFE_APPLICATION_ROOT_RE = /^(\/[\w\-.][\w\-./]*)?$/;
|
||||
|
||||
/**
|
||||
* The application root (SUPERSET_APP_ROOT) is reflected into links and
|
||||
* navigation, so constrain it to a plain absolute path before use. Anything
|
||||
* that isn't a simple "/path" prefix falls back to the default root so a
|
||||
* malformed value can't be reinterpreted as HTML or redirect off-origin. This
|
||||
* also keeps the bootstrap-derived value from being treated as a tainted href
|
||||
* source by static analysis.
|
||||
*/
|
||||
const sanitizeApplicationRoot = (
|
||||
path: string | undefined,
|
||||
fallback: string,
|
||||
): string => {
|
||||
const normalizedFallback = normalizePathWithFallback(fallback, fallback);
|
||||
const normalized = normalizePathWithFallback(path, fallback);
|
||||
return SAFE_APPLICATION_ROOT_RE.test(normalized)
|
||||
? normalized
|
||||
: normalizedFallback;
|
||||
};
|
||||
|
||||
const APPLICATION_ROOT_NO_TRAILING_SLASH = sanitizeApplicationRoot(
|
||||
getBootstrapData().common.application_root,
|
||||
DEFAULT_BOOTSTRAP_DATA.common.application_root,
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { bindActionCreators } from 'redux';
|
||||
@@ -111,6 +112,7 @@ const App = () => (
|
||||
</Suspense>
|
||||
</Route>
|
||||
))}
|
||||
<Redirect from="/" to="/superset/welcome/" exact />
|
||||
</Switch>
|
||||
</ExtensionsStartup>
|
||||
<ToastContainer />
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface Dashboard {
|
||||
certified_by?: string;
|
||||
certification_details?: string;
|
||||
changed_by_name: string;
|
||||
changed_on?: string;
|
||||
changed_on_delta_humanized?: string;
|
||||
changed_on_utc?: string;
|
||||
changed_by: string;
|
||||
@@ -63,7 +64,7 @@ export interface Dashboard {
|
||||
id: number;
|
||||
published: boolean;
|
||||
url: string;
|
||||
thumbnail_url: string;
|
||||
thumbnail_url?: string | null;
|
||||
owners: Owner[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user