Compare commits

..
Author SHA1 Message Date
Beto Dealmeida 8bda849d9c Fix lint 2026-05-19 20:18:50 -04:00
Beto Dealmeida cd8d1c1c92 Custom form 2026-05-19 20:14:26 -04:00
Beto Dealmeida 00d75f962b OAuth2 2026-05-19 19:43:37 -04:00
Beto Dealmeida 3fb7c26b66 Pass configuration 2026-05-19 16:34:57 -04:00
Beto Dealmeida 41bf352f95 feat: semantic layer DB engine spec 2026-05-19 16:08:24 -04:00
143 changed files with 1750 additions and 7658 deletions
+1 -1
View File
@@ -38,7 +38,7 @@
# Notify translation maintainers of changes to translations
/superset/translations/ @sfirke @rusackas
/superset/translations/ @sfirke
# Notify PMC members of changes to extension-related files
-5
View File
@@ -62,11 +62,6 @@ updates:
- package-ecosystem: "pip"
directory: "/"
open-pull-requests-limit: 10
# Bump the lower bound to the new version, not just widen the upper
# bound. Without this, a `sqlglot>=28.10.0, <29` constraint upgraded
# to `<30` would keep the stale lower bound forever, dragging
# transitively-resolved versions with it. See #40186 (review thread).
versioning-strategy: increase
schedule:
interval: "weekly"
labels:
@@ -53,7 +53,7 @@ jobs:
- name: Upload coverage reports to Codecov
if: steps.check.outputs.superset-extensions-cli
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
file: ./coverage.xml
flags: superset-extensions-cli
+1 -1
View File
@@ -128,7 +128,7 @@ jobs:
run: npx nyc merge coverage/ merged-output/coverage-summary.json
- name: Upload Code Coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
flags: javascript
use_oidc: true
@@ -70,7 +70,7 @@ jobs:
run: |
./scripts/python_tests.sh
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
flags: python,mysql
verbose: true
@@ -164,7 +164,7 @@ jobs:
run: |
./scripts/python_tests.sh
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
flags: python,postgres
verbose: true
@@ -219,7 +219,7 @@ jobs:
run: |
./scripts/python_tests.sh
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
flags: python,sqlite
verbose: true
@@ -79,7 +79,7 @@ jobs:
run: |
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
flags: python,presto
verbose: true
@@ -150,7 +150,7 @@ jobs:
pip install -e .[hive]
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
flags: python,hive
verbose: true
@@ -56,7 +56,7 @@ jobs:
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
flags: python,unit
verbose: true
@@ -36,9 +36,9 @@ Screenshots will be taken but no messages actually sent as long as `ALERT_REPORT
#### In your `Dockerfile`
You'll need to extend the Superset image to include a headless browser. Your options include:
- Use Playwright with Chromium: this is the recommended approach as of version 4.1.x or greater. Playwright always uses Chromium — the `WEBDRIVER_TYPE` config setting has no effect when Playwright is active. A working example of a Dockerfile that installs these tools is provided under "Building your own production Docker image" on the [Docker Builds](/admin-docs/installation/docker-builds#building-your-own-production-docker-image) page. Enable the `PLAYWRIGHT_REPORTS_AND_THUMBNAILS` feature flag in your config to activate it.
- Use Firefox (Selenium): you'll need to install geckodriver and Firefox. Set `WEBDRIVER_TYPE` to `"firefox"` in your `superset_config.py`.
- Use Chrome (Selenium): you'll need to install Chrome. Set `WEBDRIVER_TYPE` to `"chrome"` in your `superset_config.py`.
- Use Playwright with Chrome: this is the recommended approach as of version 4.1.x or greater. A working example of a Dockerfile that installs these tools is provided under "Building your own production Docker image" on the [Docker Builds](/admin-docs/installation/docker-builds#building-your-own-production-docker-image) page. Read the code comments there as you'll also need to change a feature flag in your config.
- Use Firefox: you'll need to install geckodriver and Firefox.
- Use Chrome without Playwright: you'll need to install Chrome and set the value of `WEBDRIVER_TYPE` to `"chrome"` in your `superset_config.py`.
In Superset versions &lt;=4.0x, users installed Firefox or Chrome and that was documented here.
+3 -3
View File
@@ -72,7 +72,7 @@
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.33",
"antd": "^6.4.3",
"baseline-browser-mapping": "^2.10.31",
"baseline-browser-mapping": "^2.10.30",
"caniuse-lite": "^1.0.30001793",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
@@ -109,8 +109,8 @@
"globals": "^17.6.0",
"prettier": "^3.8.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.59.4",
"webpack": "^5.107.0"
"typescript-eslint": "^8.59.3",
"webpack": "^5.106.2"
},
"browserslist": {
"production": [
+5 -19
View File
@@ -206,26 +206,12 @@ async function downloadBadge(url, staticDir) {
badgeCache.set(url, webPath);
return webPath;
} catch (error) {
// Soft fallback: keep the original remote URL in the rendered output
// so the badge still appears for readers, and the docs build continues.
// External badge services (notably img.shields.io) rate-limit CI IPs
// aggressively, and a transient fetch failure shouldn't take the whole
// docs build down with it. Set REMARK_BADGES_STRICT=true to opt back
// into hard-fail-the-build behavior (e.g. for release builds where you
// want to catch genuinely broken badge URLs).
if (process.env.REMARK_BADGES_STRICT === 'true') {
throw new Error(
`[remark-localize-badges] Failed to download badge: ${url}\n` +
`Error: ${error.message}\n` +
`Build cannot continue with broken badges (REMARK_BADGES_STRICT=true).`,
);
}
console.warn(
`[remark-localize-badges] Could not localize ${url} ` +
`(${error.message}); falling back to remote URL.`,
// Fail the build on badge download failure
throw new Error(
`[remark-localize-badges] Failed to download badge: ${url}\n` +
`Error: ${error.message}\n` +
`Build cannot continue with broken badges. Please fix the badge URL or remove it.`,
);
badgeCache.set(url, url);
return url;
} finally {
// Clean up the in-flight tracker
inFlightDownloads.delete(url);
+127 -110
View File
@@ -4455,6 +4455,22 @@
dependencies:
"@types/ms" "*"
"@types/eslint-scope@^3.7.7":
version "3.7.7"
resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz"
integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==
dependencies:
"@types/eslint" "*"
"@types/estree" "*"
"@types/eslint@*":
version "9.6.1"
resolved "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz"
integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
"@types/estree-jsx@^1.0.0":
version "1.0.5"
resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz"
@@ -4596,7 +4612,7 @@
resolved "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.4.tgz"
integrity sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==
"@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.15"
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@@ -4812,100 +4828,100 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.59.4", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz#c67bfee32caae9cb587dce1ac59c3bf43b659707"
integrity sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==
"@typescript-eslint/eslint-plugin@8.59.3", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.59.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz#5d6da7e7b236b46452fa00d3904bb6f59615bfde"
integrity sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.59.4"
"@typescript-eslint/type-utils" "8.59.4"
"@typescript-eslint/utils" "8.59.4"
"@typescript-eslint/visitor-keys" "8.59.4"
"@typescript-eslint/scope-manager" "8.59.3"
"@typescript-eslint/type-utils" "8.59.3"
"@typescript-eslint/utils" "8.59.3"
"@typescript-eslint/visitor-keys" "8.59.3"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.59.4", "@typescript-eslint/parser@^8.59.3":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.4.tgz#77d99e3b27663e7a22cf12c3fb769db509e5e93c"
integrity sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==
"@typescript-eslint/parser@8.59.3", "@typescript-eslint/parser@^8.59.3":
version "8.59.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.3.tgz#f46cbc70ae0a25119ef94eac9ecd46714788e1a1"
integrity sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==
dependencies:
"@typescript-eslint/scope-manager" "8.59.4"
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/typescript-estree" "8.59.4"
"@typescript-eslint/visitor-keys" "8.59.4"
"@typescript-eslint/scope-manager" "8.59.3"
"@typescript-eslint/types" "8.59.3"
"@typescript-eslint/typescript-estree" "8.59.3"
"@typescript-eslint/visitor-keys" "8.59.3"
debug "^4.4.3"
"@typescript-eslint/project-service@8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.4.tgz#5830535a0e7a3ae806e2669964f47a74c4bc6b0e"
integrity sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==
"@typescript-eslint/project-service@8.59.3":
version "8.59.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.3.tgz#1be5ae152aad987a156c9a1a9b4256e75cfbbe0c"
integrity sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.59.4"
"@typescript-eslint/types" "^8.59.4"
"@typescript-eslint/tsconfig-utils" "^8.59.3"
"@typescript-eslint/types" "^8.59.3"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz#507d1258c758147dac1adee9517a205a8ac1e046"
integrity sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==
"@typescript-eslint/scope-manager@8.59.3":
version "8.59.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz#91a60f66803fe9dae0696fbab2451f5723f119d2"
integrity sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==
dependencies:
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/visitor-keys" "8.59.4"
"@typescript-eslint/types" "8.59.3"
"@typescript-eslint/visitor-keys" "8.59.3"
"@typescript-eslint/tsconfig-utils@8.59.4", "@typescript-eslint/tsconfig-utils@^8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz#218ba229d96dde35212e3a76a7d0a6bc831398be"
integrity sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==
"@typescript-eslint/tsconfig-utils@8.59.3", "@typescript-eslint/tsconfig-utils@^8.59.3":
version "8.59.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz#88ca9036b42ccdd1e630cfdafd2e042c2ca6a835"
integrity sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==
"@typescript-eslint/type-utils@8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz#359fc53ba39a1f1860fddda40ebe5bfe0d87faed"
integrity sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==
"@typescript-eslint/type-utils@8.59.3":
version "8.59.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz#421fb2448bdfeb301d134a01cd02503f67fd8192"
integrity sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==
dependencies:
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/typescript-estree" "8.59.4"
"@typescript-eslint/utils" "8.59.4"
"@typescript-eslint/types" "8.59.3"
"@typescript-eslint/typescript-estree" "8.59.3"
"@typescript-eslint/utils" "8.59.3"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.59.4", "@typescript-eslint/types@^8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.4.tgz#c29d5c21bfbaa8347ddc677d3ac1fcd2db0f848e"
integrity sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==
"@typescript-eslint/types@8.59.3", "@typescript-eslint/types@^8.59.3":
version "8.59.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.3.tgz#b7ca539c5e302fdde9a7cadb73caed107ef8f2cd"
integrity sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==
"@typescript-eslint/typescript-estree@8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz#d005e5e1fb425526f39685594bed34a04ad755ea"
integrity sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==
"@typescript-eslint/typescript-estree@8.59.3":
version "8.59.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz#e6bb1408e00b47e431427a40268db4e86cb121ab"
integrity sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==
dependencies:
"@typescript-eslint/project-service" "8.59.4"
"@typescript-eslint/tsconfig-utils" "8.59.4"
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/visitor-keys" "8.59.4"
"@typescript-eslint/project-service" "8.59.3"
"@typescript-eslint/tsconfig-utils" "8.59.3"
"@typescript-eslint/types" "8.59.3"
"@typescript-eslint/visitor-keys" "8.59.3"
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.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.4.tgz#8ccd2b08aecc72c7efc0d7ac6695631d199d256e"
integrity sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==
"@typescript-eslint/utils@8.59.3":
version "8.59.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.3.tgz#f693f979deb4dc3994de03ff8b23976d625c36c5"
integrity sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.59.4"
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/typescript-estree" "8.59.4"
"@typescript-eslint/scope-manager" "8.59.3"
"@typescript-eslint/types" "8.59.3"
"@typescript-eslint/typescript-estree" "8.59.3"
"@typescript-eslint/visitor-keys@8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz#1ac23b747b011f5cbdb449da97769f6c5f3a9355"
integrity sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==
"@typescript-eslint/visitor-keys@8.59.3":
version "8.59.3"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz#820843b1b5ca4290009cf189382abcf6fe00dfa6"
integrity sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==
dependencies:
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/types" "8.59.3"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -5568,10 +5584,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.31, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.31"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz#9c6825f052601ce6974a90dd49683b1726887b0b"
integrity sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==
baseline-browser-mapping@^2.10.30, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.30"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz#58915c74388b05f3b3504026194ea9fa98f6e6b6"
integrity sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==
batch@0.6.1:
version "0.6.1"
@@ -7253,13 +7269,13 @@ encodeurl@~2.0.0:
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.21.4:
version "5.21.5"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.5.tgz#8f80167d009d8f01267ad61035e59fe5c94ac3a6"
integrity sha512-mLCNbrQli11K1ySUmuNt4ZUB3OpGIDq4q2vTBTf5cL2lpsRjI9QKqSD0ndjW8FyvcW/Jj46gMe9syyHAsvMa/A==
enhanced-resolve@^5.20.0:
version "5.20.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz#323c2a70d2aa7fb4bdfd6d3c24dfc705c581295d"
integrity sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.3.3"
tapable "^2.3.0"
entities@^2.0.0:
version "2.2.0"
@@ -7375,10 +7391,10 @@ es-iterator-helpers@^1.2.1:
iterator.prototype "^1.1.4"
safe-array-concat "^1.1.3"
es-module-lexer@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz#1dfcbb5ea3bbfb63f28e1fc3676c3676d1c9624c"
integrity sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==
es-module-lexer@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz"
integrity sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
@@ -9639,10 +9655,10 @@ liquid-json@0.3.1:
resolved "https://registry.npmjs.org/liquid-json/-/liquid-json-0.3.1.tgz"
integrity sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==
loader-runner@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.2.tgz#9913d3a15971f8f635915e601fb5c9d495d918e9"
integrity sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==
loader-runner@^4.3.1:
version "4.3.1"
resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz"
integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==
loader-utils@^1.2.3:
version "1.4.2"
@@ -14112,15 +14128,15 @@ synckit@^0.11.12:
dependencies:
"@pkgr/core" "^0.2.9"
tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0, tapable@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160"
integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==
tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz"
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
terser-webpack-plugin@^5.3.9, terser-webpack-plugin@^5.5.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz#8e7caad248183ab9e91ff08a83b0fc9f0439c3c3"
integrity sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==
terser-webpack-plugin@^5.3.17, terser-webpack-plugin@^5.3.9:
version "5.3.17"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz#75ea98876297fbb190d2fbb395e982582b859a67"
integrity sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==
dependencies:
"@jridgewell/trace-mapping" "^0.3.25"
jest-worker "^27.4.5"
@@ -14391,15 +14407,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.59.4:
version "8.59.4"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.4.tgz#834e3b53f4d1a764a985ceb8592c4a95d6a8da7c"
integrity sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==
typescript-eslint@^8.59.3:
version "8.59.3"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.3.tgz#4a41d9007faa539a66292189e2795eeb0b9fca29"
integrity sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==
dependencies:
"@typescript-eslint/eslint-plugin" "8.59.4"
"@typescript-eslint/parser" "8.59.4"
"@typescript-eslint/typescript-estree" "8.59.4"
"@typescript-eslint/utils" "8.59.4"
"@typescript-eslint/eslint-plugin" "8.59.3"
"@typescript-eslint/parser" "8.59.3"
"@typescript-eslint/typescript-estree" "8.59.3"
"@typescript-eslint/utils" "8.59.3"
typescript@~6.0.3:
version "6.0.3"
@@ -14954,21 +14970,22 @@ webpack-merge@^6.0.1:
flat "^5.0.2"
wildcard "^2.0.1"
webpack-sources@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.4.1.tgz#009d110999ebd9fb3a6fa8d32eec6f84d940e65d"
integrity sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==
webpack-sources@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.4.tgz#a338b95eb484ecc75fbb196cbe8a2890618b4891"
integrity sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==
webpack-virtual-modules@^0.6.2:
version "0.6.2"
resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
webpack@^5.107.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.107.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.107.0.tgz#9e0d8d8baf24e76f058103f4f06ac6bb528b645a"
integrity sha512-PSxeHk/dmLYZlnTU+vL1Gej6Evg5RNtl3flhxBresfznFnzxinHMzHKloHnywM/3ouQv7/AlZCswWDIkNSggUA==
webpack@^5.106.2, webpack@^5.88.1, webpack@^5.95.0:
version "5.106.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.106.2.tgz#ca8174b4fd80f055cc5a45fcc5577d6db76c8ac5"
integrity sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
"@types/json-schema" "^7.0.15"
"@webassemblyjs/ast" "^1.14.1"
@@ -14978,20 +14995,20 @@ webpack@^5.107.0, webpack@^5.88.1, webpack@^5.95.0:
acorn-import-phases "^1.0.3"
browserslist "^4.28.1"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.21.4"
es-module-lexer "^2.1.0"
enhanced-resolve "^5.20.0"
es-module-lexer "^2.0.0"
eslint-scope "5.1.1"
events "^3.2.0"
glob-to-regexp "^0.4.1"
graceful-fs "^4.2.11"
loader-runner "^4.3.2"
loader-runner "^4.3.1"
mime-db "^1.54.0"
neo-async "^2.6.2"
schema-utils "^4.3.3"
tapable "^2.3.0"
terser-webpack-plugin "^5.5.0"
terser-webpack-plugin "^5.3.17"
watchpack "^2.5.1"
webpack-sources "^3.4.1"
webpack-sources "^3.3.4"
webpackbar@^7.0.0:
version "7.0.0"
@@ -92,26 +92,6 @@ class Dimension:
grain: Grain | None = None
class AggregationType(str, enum.Enum):
"""
Aggregation function applied by a metric.
Additivity (across an arbitrary set of grouping dimensions):
* ``SUM``, ``COUNT``: fully additive — sub-group sums roll up via ``sum``.
* ``MIN``, ``MAX``: roll up via ``min`` / ``max`` of sub-group values.
* ``AVG``, ``COUNT_DISTINCT``, ``OTHER``: not safely roll-uppable from
sub-aggregates without auxiliary data.
"""
SUM = "SUM"
COUNT = "COUNT"
MIN = "MIN"
MAX = "MAX"
AVG = "AVG"
COUNT_DISTINCT = "COUNT_DISTINCT"
OTHER = "OTHER"
@dataclass(frozen=True)
class Metric:
id: str
@@ -120,7 +100,6 @@ class Metric:
definition: str
description: str | None = None
aggregation: AggregationType | None = None
@dataclass(frozen=True)
@@ -17,8 +17,12 @@
* under the License.
*/
import { ReactNode, SyntheticEvent } from 'react';
import { ResizableBox, ResizeCallbackData } from 'react-resizable';
import { PropsWithChildren, ReactNode, SyntheticEvent } from 'react';
import {
ResizableBox,
ResizableBoxProps,
ResizeCallbackData,
} from 'react-resizable';
import { styled } from '@apache-superset/core/theme';
import 'react-resizable/css/styles.css';
@@ -42,16 +46,14 @@ export type Size = ResizeCallbackData['size'];
export default function ResizablePanel({
children,
heading,
heading = undefined,
initialSize = { width: 500, height: 300 },
minConstraints = [100, 100] as [number, number],
onResize,
}: {
children?: ReactNode;
...props
}: PropsWithChildren<Omit<ResizableBoxProps, 'width' | 'height'>> & {
heading?: ReactNode;
initialSize?: Size;
minConstraints?: [number, number];
onResize?: (e: SyntheticEvent, data: ResizeCallbackData) => void;
}) {
const { width, height } = initialSize;
return (
@@ -59,14 +61,16 @@ export default function ResizablePanel({
className="panel"
width={width}
height={height}
axis="both"
minConstraints={minConstraints}
maxConstraints={[Infinity, Infinity]}
handleSize={[20, 20]}
lockAspectRatio={false}
resizeHandles={['se']}
transformScale={1}
onResize={onResize}
onResize={
onResize
? (e: SyntheticEvent, data: ResizeCallbackData) => {
const { size } = data;
onResize(e, { ...data, size });
}
: undefined
}
{...props}
>
<>
{heading ? <div className="panel-heading">{heading}</div> : null}
+247 -242
View File
@@ -85,7 +85,7 @@
"antd": "^5.26.0",
"chrono-node": "^2.9.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.0",
"content-disposition": "^1.1.0",
"d3-color": "^3.1.0",
"d3-scale": "^4.0.2",
"dayjs": "^1.11.20",
@@ -109,8 +109,8 @@
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.18.1",
"mapbox-gl": "^3.24.0",
"markdown-to-jsx": "^9.8.1",
"mapbox-gl": "^3.23.1",
"markdown-to-jsx": "^9.8.0",
"match-sorter": "^8.3.0",
"memoize-one": "^5.2.1",
"mousetrap": "^1.6.5",
@@ -208,12 +208,12 @@
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.9.1",
"@types/node": "^25.8.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^4.0.0",
"@types/react-resizable": "^3.0.8",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.12",
"@types/react-window": "^1.8.8",
@@ -222,8 +222,8 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
@@ -269,13 +269,13 @@
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
"open-cli": "^9.0.0",
"oxlint": "^1.66.0",
"oxlint": "^1.65.0",
"po2json": "^0.4.5",
"prettier": "3.8.3",
"prettier-plugin-packagejson": "^3.0.2",
"process": "^0.11.10",
"react-refresh": "^0.18.0",
"react-resizable": "^4.0.1",
"react-resizable": "^3.1.3",
"redux-mock-store": "^1.5.4",
"source-map": "^0.7.6",
"source-map-support": "^0.5.21",
@@ -284,9 +284,9 @@
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.0",
"ts-jest": "^29.4.10",
"ts-jest": "^29.4.9",
"tscw-config": "^1.1.2",
"tsx": "^4.22.3",
"tsx": "^4.22.0",
"typescript": "5.4.5",
"unzipper": "^0.12.3",
"vm-browserify": "^1.1.2",
@@ -8646,9 +8646,9 @@
"license": "MIT"
},
"node_modules/@oxlint/binding-android-arm-eabi": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.66.0.tgz",
"integrity": "sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.65.0.tgz",
"integrity": "sha512-jDVaGNURT5pEA9qcabh6WusIoBNybOMMDPCx+EFt+gxo6rVvoUf0+73Xy5x81+ZrxU+ewk5uRBYifjy5pgkcnA==",
"cpu": [
"arm"
],
@@ -8663,9 +8663,9 @@
}
},
"node_modules/@oxlint/binding-android-arm64": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.66.0.tgz",
"integrity": "sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.65.0.tgz",
"integrity": "sha512-v0z80IWNA7c9RhUydq9YprBxCVZrQ6Ixls2tdxUC1F/1FFqSfa7xTX+EJf0mj6+BKRg2zWXqWfcbJUnETlLlIw==",
"cpu": [
"arm64"
],
@@ -8680,9 +8680,9 @@
}
},
"node_modules/@oxlint/binding-darwin-arm64": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.66.0.tgz",
"integrity": "sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.65.0.tgz",
"integrity": "sha512-pL/mG/5gMzBwp1gdc5+Cwi87F9j3XRnPxHGyVj5Zd+dCEV5YkKt0L70PB3EGmEEHxgn4H+jnMS3xLuXs6mZW/Q==",
"cpu": [
"arm64"
],
@@ -8697,9 +8697,9 @@
}
},
"node_modules/@oxlint/binding-darwin-x64": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.66.0.tgz",
"integrity": "sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.65.0.tgz",
"integrity": "sha512-jVTneaeuHtqTrKYnhrdH1buhnSorinvpy1sv43ayclfWx/e/DfdRWv+h1fopJcHQbYr5WMcZMmDvnfEBkPZ+1A==",
"cpu": [
"x64"
],
@@ -8714,9 +8714,9 @@
}
},
"node_modules/@oxlint/binding-freebsd-x64": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.66.0.tgz",
"integrity": "sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.65.0.tgz",
"integrity": "sha512-8lJQ7B6RloYDUhwVdbSpwT2eKsCN5KP1Scn18ly1tytCuhXhbs0nkfKHT4jWWZBJqmynWuzd+78bF7wILrj6pw==",
"cpu": [
"x64"
],
@@ -8731,9 +8731,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.66.0.tgz",
"integrity": "sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.65.0.tgz",
"integrity": "sha512-EgmZY+DeWhLLEnNl70/49j3ltA8I6X9kxMfexupWi2Vwfp6RonGsBaHtGoedLolaU37ne7eDUgoxa3CFB95GZA==",
"cpu": [
"arm"
],
@@ -8748,9 +8748,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.66.0.tgz",
"integrity": "sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.65.0.tgz",
"integrity": "sha512-OJMWmAYRVBCPPxnYr3j5sXRwHPh1bAuMlTStGco1Z8q3HkvSH4h+A10E9MiRNYmLhUuli5a2P5wmfj8cagiF5Q==",
"cpu": [
"arm"
],
@@ -8765,9 +8765,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-gnu": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.66.0.tgz",
"integrity": "sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.65.0.tgz",
"integrity": "sha512-D8uNi50LsYKgS0vGARZDRx05TBZeSxAVdLGddSEqQLSU7xsiqdImHPEw55xq8sKA5rCc/4au/5uS7FQALWdLCg==",
"cpu": [
"arm64"
],
@@ -8782,9 +8782,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-musl": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.66.0.tgz",
"integrity": "sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.65.0.tgz",
"integrity": "sha512-IpbA8QGbwFehQhO+YaHwmoI81f93xvywpspf8HrdPCWOIeKwYfM1dhVhO4YKfZewTRRQEPY/JFjTOXTgkwhKrA==",
"cpu": [
"arm64"
],
@@ -8799,9 +8799,9 @@
}
},
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.66.0.tgz",
"integrity": "sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.65.0.tgz",
"integrity": "sha512-ZSe8HgaZdgyHSv2+/pTG68z10+OarB18CkFKQOhRs3lmmP/p2vuigedK2e9d0ztoG2DU/duJzhxXBSjy/492HQ==",
"cpu": [
"ppc64"
],
@@ -8816,9 +8816,9 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.66.0.tgz",
"integrity": "sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.65.0.tgz",
"integrity": "sha512-DcTERf++v6HyPHukKAr0JFTRqB+YeDEvqzRgNDMaz7jITPf+tlJIwRxodlAqoXMYhNVEZhXdQM5RAAYH8/oPuw==",
"cpu": [
"riscv64"
],
@@ -8833,9 +8833,9 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-musl": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.66.0.tgz",
"integrity": "sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.65.0.tgz",
"integrity": "sha512-xjhMwuFJwRh40NOBzol4gM5gqAa0xPCJU+GQLM6BydV8TbfkIA7JeyCFNhyfbE9Q/5EWcKYTx62R0cRcjP7DAA==",
"cpu": [
"riscv64"
],
@@ -8850,9 +8850,9 @@
}
},
"node_modules/@oxlint/binding-linux-s390x-gnu": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.66.0.tgz",
"integrity": "sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.65.0.tgz",
"integrity": "sha512-lrWSXb8JzboPWYBG6Kunt/eemvjo2oCFXktShsm3yMToY7HjzKLjxh7CljSvGnnZH9oohNFHOKc9xYpGKCPm6w==",
"cpu": [
"s390x"
],
@@ -8867,9 +8867,9 @@
}
},
"node_modules/@oxlint/binding-linux-x64-gnu": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.66.0.tgz",
"integrity": "sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.65.0.tgz",
"integrity": "sha512-A7xfghw250m4a1sPV+q44Mow2G5bhiC9FBvhAuIhJS6QovWnqzuL5AFQPEuwOB+PM4DhABkqxVa3Iwe3Y/nFlQ==",
"cpu": [
"x64"
],
@@ -8884,9 +8884,9 @@
}
},
"node_modules/@oxlint/binding-linux-x64-musl": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.66.0.tgz",
"integrity": "sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.65.0.tgz",
"integrity": "sha512-reqOun1+pWO3fW6cv7bsa8hHG0TN3t/82qPdaoJo90FwugXiMjKhZMChmH5Z01cFNRHmxN4+543Fy8478cM/iA==",
"cpu": [
"x64"
],
@@ -8901,9 +8901,9 @@
}
},
"node_modules/@oxlint/binding-openharmony-arm64": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.66.0.tgz",
"integrity": "sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.65.0.tgz",
"integrity": "sha512-KQpqOb/juDBO0xyloDkVDhOVxDUgAfZ2OAAVq99TJScJDzT319xry1QzB9LQohV9QGnA7p6m/XATZkMXc84lwA==",
"cpu": [
"arm64"
],
@@ -8918,9 +8918,9 @@
}
},
"node_modules/@oxlint/binding-win32-arm64-msvc": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.66.0.tgz",
"integrity": "sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.65.0.tgz",
"integrity": "sha512-xfqcOc3nJFeAd1kDY4T9d3XeJIhr00twaaW0kOAzGPyUHkruXtNJv6zz1Ra9fRtSek5VpW2Yoj5AcwPIlT0ZiQ==",
"cpu": [
"arm64"
],
@@ -8935,9 +8935,9 @@
}
},
"node_modules/@oxlint/binding-win32-ia32-msvc": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.66.0.tgz",
"integrity": "sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.65.0.tgz",
"integrity": "sha512-JV+pXm45p8sdgs3c7LOPAohW23optCNZETFOXUcjn6cS4PYZhEU/RI54Z5dHdMudab3nw7T48PZILthM+Q0COQ==",
"cpu": [
"ia32"
],
@@ -8952,9 +8952,9 @@
}
},
"node_modules/@oxlint/binding-win32-x64-msvc": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.66.0.tgz",
"integrity": "sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.65.0.tgz",
"integrity": "sha512-D7L/oBbskLss21bYrRbFuIs81AiSQV+wRzwck54dOkHIlq2qu1xjLz8u6jCqGH8Fltk8bB5DLBpVhE7v/fA8XQ==",
"cpu": [
"x64"
],
@@ -13639,9 +13639,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"license": "MIT",
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
@@ -13796,14 +13796,12 @@
}
},
"node_modules/@types/react-resizable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-4.0.0.tgz",
"integrity": "sha512-zEMfyoFP/I6L796+dIjXeAVL2q7C4vwCXwQW2be79uPZAQLsI/pU1NOVqqfVQG3FkIxr/n96KjhzdRnWGZcTcg==",
"deprecated": "This is a stub types definition. react-resizable provides its own type definitions, so you do not need this installed.",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-3.0.8.tgz",
"integrity": "sha512-Pcvt2eGA7KNXldt1hkhVhAgZ8hK41m0mp89mFgQi7LAAEZiaLgm4fHJ5zbJZ/4m2LVaAyYrrRRv1LHDcrGQanA==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-resizable": "*"
"@types/react": "*"
}
},
"node_modules/@types/react-router": {
@@ -14123,17 +14121,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz",
"integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/type-utils": "8.59.4",
"@typescript-eslint/utils": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/type-utils": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -14146,20 +14144,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.59.4",
"@typescript-eslint/parser": "^8.59.3",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz",
"integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.59.4",
"@typescript-eslint/types": "^8.59.4",
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"debug": "^4.4.3"
},
"engines": {
@@ -14174,14 +14172,14 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4"
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14192,9 +14190,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz",
"integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14209,9 +14207,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14223,16 +14221,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz",
"integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.59.4",
"@typescript-eslint/tsconfig-utils": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/project-service": "8.59.3",
"@typescript-eslint/tsconfig-utils": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -14251,16 +14249,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz",
"integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4"
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14275,13 +14273,13 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/types": "8.59.3",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -14355,16 +14353,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz",
"integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3"
},
"engines": {
@@ -14380,14 +14378,14 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz",
"integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.59.4",
"@typescript-eslint/types": "^8.59.4",
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"debug": "^4.4.3"
},
"engines": {
@@ -14402,14 +14400,14 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4"
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14420,9 +14418,9 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz",
"integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14437,9 +14435,9 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14451,16 +14449,16 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz",
"integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.59.4",
"@typescript-eslint/tsconfig-utils": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/project-service": "8.59.3",
"@typescript-eslint/tsconfig-utils": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -14479,13 +14477,13 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/types": "8.59.3",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -14620,15 +14618,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz",
"integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/utils": "8.59.4",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -14645,14 +14643,14 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz",
"integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.59.4",
"@typescript-eslint/types": "^8.59.4",
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"debug": "^4.4.3"
},
"engines": {
@@ -14667,14 +14665,14 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4"
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14685,9 +14683,9 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz",
"integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14702,9 +14700,9 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14716,16 +14714,16 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz",
"integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.59.4",
"@typescript-eslint/tsconfig-utils": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/project-service": "8.59.3",
"@typescript-eslint/tsconfig-utils": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -14744,16 +14742,16 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz",
"integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4"
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14768,13 +14766,13 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/types": "8.59.3",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -18960,9 +18958,9 @@
"license": "MIT"
},
"node_modules/content-disposition": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.0.tgz",
"integrity": "sha512-qqGFOrKmFP1lTfG24opOJFcTMza1BqyTSUKVbMGUP5uRsBH+C00Q1loOk+JSFshyRE0ji4HtCJeNN2WHWd6PGw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -25496,6 +25494,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/grid-index": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz",
"integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==",
"license": "ISC"
},
"node_modules/h3-js": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.2.1.tgz",
@@ -32878,13 +32882,13 @@
"license": "MIT"
},
"node_modules/mapbox-gl": {
"version": "3.24.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.24.0.tgz",
"integrity": "sha512-R+FdFUB3DnoE5FYASV7lGSiRyMkSblcZ2UEy7b2pt7s5ZbCxFIUPXd0E6iAFd8OdvdA2VtbvZZVylzAZNaurjA==",
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.23.1.tgz",
"integrity": "sha512-J5M32GunL5KcxvV3ZyLJdr7xtcQ3afAO3VjitLW0v2UVfHjXhq9NO4tkTpn4Dknqwq/pXZG7j1WBfmddLCA26w==",
"license": "SEE LICENSE IN LICENSE.txt",
"workspaces": [
"src/style-spec",
"plugins/mapbox-gl-pmtiles-provider",
"packages/pmtiles-provider",
"test/build/vite",
"test/build/webpack",
"test/build/typings"
@@ -32904,6 +32908,7 @@
"earcut": "^3.0.1",
"geojson-vt": "^4.0.2",
"gl-matrix": "^3.4.4",
"grid-index": "^1.1.0",
"kdbush": "^4.0.2",
"martinez-polygon-clipping": "^0.8.1",
"murmurhash-js": "^1.0.0",
@@ -33038,9 +33043,9 @@
}
},
"node_modules/markdown-to-jsx": {
"version": "9.8.1",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.8.1.tgz",
"integrity": "sha512-yq70dLPkBnE2LYFtGTLfRes4qyBDS+a4wDttAA/b/BzVGrbs2e0TfCeSFrMkapCg1lsxYi+42BowuBDxLP9k4Q==",
"version": "9.8.0",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.8.0.tgz",
"integrity": "sha512-NWL0vDt6BeQgCxc2kK6g2SEPJpQuacu+NJk5j4QT4wlNmbsHdudoAbTGLCIyoPhfMiEqD1OZVBsYK0hvCky/HQ==",
"license": "MIT",
"engines": {
"node": ">= 18"
@@ -37470,9 +37475,9 @@
}
},
"node_modules/oxlint": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.66.0.tgz",
"integrity": "sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.65.0.tgz",
"integrity": "sha512-ChUuE3Q7XnAbscvT4XLMsH7HFJmLgLVv9lu+RRgFL5wSXnDqUOzTp5IS8qWDBGd/ZDSzQ2tbX8fjAmijlGLC7A==",
"dev": true,
"license": "MIT",
"bin": {
@@ -37485,25 +37490,25 @@
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxlint/binding-android-arm-eabi": "1.66.0",
"@oxlint/binding-android-arm64": "1.66.0",
"@oxlint/binding-darwin-arm64": "1.66.0",
"@oxlint/binding-darwin-x64": "1.66.0",
"@oxlint/binding-freebsd-x64": "1.66.0",
"@oxlint/binding-linux-arm-gnueabihf": "1.66.0",
"@oxlint/binding-linux-arm-musleabihf": "1.66.0",
"@oxlint/binding-linux-arm64-gnu": "1.66.0",
"@oxlint/binding-linux-arm64-musl": "1.66.0",
"@oxlint/binding-linux-ppc64-gnu": "1.66.0",
"@oxlint/binding-linux-riscv64-gnu": "1.66.0",
"@oxlint/binding-linux-riscv64-musl": "1.66.0",
"@oxlint/binding-linux-s390x-gnu": "1.66.0",
"@oxlint/binding-linux-x64-gnu": "1.66.0",
"@oxlint/binding-linux-x64-musl": "1.66.0",
"@oxlint/binding-openharmony-arm64": "1.66.0",
"@oxlint/binding-win32-arm64-msvc": "1.66.0",
"@oxlint/binding-win32-ia32-msvc": "1.66.0",
"@oxlint/binding-win32-x64-msvc": "1.66.0"
"@oxlint/binding-android-arm-eabi": "1.65.0",
"@oxlint/binding-android-arm64": "1.65.0",
"@oxlint/binding-darwin-arm64": "1.65.0",
"@oxlint/binding-darwin-x64": "1.65.0",
"@oxlint/binding-freebsd-x64": "1.65.0",
"@oxlint/binding-linux-arm-gnueabihf": "1.65.0",
"@oxlint/binding-linux-arm-musleabihf": "1.65.0",
"@oxlint/binding-linux-arm64-gnu": "1.65.0",
"@oxlint/binding-linux-arm64-musl": "1.65.0",
"@oxlint/binding-linux-ppc64-gnu": "1.65.0",
"@oxlint/binding-linux-riscv64-gnu": "1.65.0",
"@oxlint/binding-linux-riscv64-musl": "1.65.0",
"@oxlint/binding-linux-s390x-gnu": "1.65.0",
"@oxlint/binding-linux-x64-gnu": "1.65.0",
"@oxlint/binding-linux-x64-musl": "1.65.0",
"@oxlint/binding-openharmony-arm64": "1.65.0",
"@oxlint/binding-win32-arm64-msvc": "1.65.0",
"@oxlint/binding-win32-ia32-msvc": "1.65.0",
"@oxlint/binding-win32-x64-msvc": "1.65.0"
},
"peerDependencies": {
"oxlint-tsgolint": ">=0.22.1"
@@ -40806,9 +40811,9 @@
}
},
"node_modules/react-resizable": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-4.0.1.tgz",
"integrity": "sha512-FR25Rcfxpi1iKiC7taIrqc1Tz6VnslqM94/IrA1LxoM5C3ap2EqaKLnCit/aKrcn3H4wfzO0nFBadFBc+SzEWA==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz",
"integrity": "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -42580,9 +42585,9 @@
}
},
"node_modules/semver": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -45223,9 +45228,9 @@
}
},
"node_modules/ts-jest": {
"version": "29.4.10",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.10.tgz",
"integrity": "sha512-vMTlTTtvz5aKZgzOoc7DQ5TzAL2fCzl8JnG1+ZpwjQa/g0xLlwE44yQ+1Cao9ZP1xVv9y5g34IFXEiqGOGFBUA==",
"version": "29.4.9",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz",
"integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -45235,7 +45240,7 @@
"json5": "^2.2.3",
"lodash.memoize": "^4.1.2",
"make-error": "^1.3.6",
"semver": "^7.8.0",
"semver": "^7.7.4",
"type-fest": "^4.41.0",
"yargs-parser": "^21.1.1"
},
@@ -45436,9 +45441,9 @@
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz",
"integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==",
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz",
"integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -48910,7 +48915,7 @@
"dependencies": {
"chalk": "^5.6.2",
"lodash-es": "^4.18.1",
"yeoman-generator": "^8.2.2",
"yeoman-generator": "^8.1.2",
"yosay": "^3.0.0"
},
"devDependencies": {
@@ -49383,7 +49388,7 @@
"react-js-cron": "^5.2.0",
"react-markdown": "^8.0.7",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^16.1.0",
"react-syntax-highlighter": "^16.1.1",
"react-ultimate-pagination": "^1.3.2",
"regenerator-runtime": "^0.14.1",
"rehype-raw": "^7.0.0",
@@ -49403,7 +49408,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.0",
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.1",
"@types/node": "^25.8.0",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -49983,7 +49988,7 @@
"acorn": "^8.16.0",
"d3-array": "^3.2.4",
"lodash": "^4.18.1",
"zod": "^4.4.1"
"zod": "^4.4.3"
},
"peerDependencies": {
"@apache-superset/core": "*",
@@ -50093,9 +50098,9 @@
"license": "Apache-2.0",
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.24.0",
"mapbox-gl": "^3.23.1",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.1",
"react-map-gl": "^8.1.0",
"supercluster": "^8.0.1"
},
"peerDependencies": {
+11 -11
View File
@@ -166,7 +166,7 @@
"antd": "^5.26.0",
"chrono-node": "^2.9.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.0",
"content-disposition": "^1.1.0",
"d3-color": "^3.1.0",
"d3-scale": "^4.0.2",
"dayjs": "^1.11.20",
@@ -190,8 +190,8 @@
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.18.1",
"mapbox-gl": "^3.24.0",
"markdown-to-jsx": "^9.8.1",
"mapbox-gl": "^3.23.1",
"markdown-to-jsx": "^9.8.0",
"match-sorter": "^8.3.0",
"memoize-one": "^5.2.1",
"mousetrap": "^1.6.5",
@@ -289,12 +289,12 @@
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.9.1",
"@types/node": "^25.8.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^4.0.0",
"@types/react-resizable": "^3.0.8",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.12",
"@types/react-window": "^1.8.8",
@@ -303,8 +303,8 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
@@ -350,13 +350,13 @@
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
"open-cli": "^9.0.0",
"oxlint": "^1.66.0",
"oxlint": "^1.65.0",
"po2json": "^0.4.5",
"prettier": "3.8.3",
"prettier-plugin-packagejson": "^3.0.2",
"process": "^0.11.10",
"react-refresh": "^0.18.0",
"react-resizable": "^4.0.1",
"react-resizable": "^3.1.3",
"redux-mock-store": "^1.5.4",
"source-map": "^0.7.6",
"source-map-support": "^0.5.21",
@@ -365,9 +365,9 @@
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.0",
"ts-jest": "^29.4.10",
"ts-jest": "^29.4.9",
"tscw-config": "^1.1.2",
"tsx": "^4.22.3",
"tsx": "^4.22.0",
"typescript": "5.4.5",
"unzipper": "^0.12.3",
"vm-browserify": "^1.1.2",
@@ -30,7 +30,7 @@
"dependencies": {
"chalk": "^5.6.2",
"lodash-es": "^4.18.1",
"yeoman-generator": "^8.2.2",
"yeoman-generator": "^8.1.2",
"yosay": "^3.0.0"
},
"devDependencies": {
@@ -56,7 +56,7 @@
"react-js-cron": "^5.2.0",
"react-markdown": "^8.0.7",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^16.1.0",
"react-syntax-highlighter": "^16.1.1",
"react-ultimate-pagination": "^1.3.2",
"regenerator-runtime": "^0.14.1",
"rehype-raw": "^7.0.0",
@@ -76,7 +76,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.0",
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.1",
"@types/node": "^25.8.0",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -63,8 +63,7 @@ export default async function parseResponse<T extends ParseMethod = 'json'>(
(value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
value?.isLessThan?.(Number.MIN_SAFE_INTEGER))
) {
// toFixed() avoids scientific notation, which BigInt() rejects.
return BigInt(value.toFixed());
return BigInt(value);
}
// // `json-bigint` could not handle floats well, see sidorares/json-bigint#62
// // TODO: clean up after json-bigint>1.0.1 is released
@@ -183,26 +183,6 @@ describe('parseResponse()', () => {
expect(responseBigNumber.json.constructor).toEqual('constructor');
});
test('handles big numbers in scientific notation when `parseMethod=json-bigint`', async () => {
const mockScientificUrl = '/mock/get/scientific';
const mockScientificPayload =
'{ "big_double": 4.799703045723905e+32, "negative_big": -4.799703045723905e+32, "small": 1 }';
fetchMock.get(mockScientificUrl, mockScientificPayload);
const responseBigNumber = await parseResponse(
callApi({ url: mockScientificUrl, method: 'GET' }),
'json-bigint',
);
expect(`${responseBigNumber.json.big_double}`).toEqual(
'479970304572390500000000000000000',
);
expect(`${responseBigNumber.json.negative_big}`).toEqual(
'-479970304572390500000000000000000',
);
expect(responseBigNumber.json.small).toEqual(1);
});
test('rejects if request.ok=false', async () => {
expect.assertions(3);
const mockNotOkayUrl = '/mock/notokay/url';
@@ -95,11 +95,8 @@ class FakeMessageChannel {
const port2 = new FakeMessagePort();
port1.otherPort = port2;
port2.otherPort = port1;
// FakeMessagePort only implements the subset of MessagePort that
// Switchboard exercises; cast at the boundary so the fake satisfies
// the consumer signature without weakening the production type.
this.port1 = port1 as unknown as MessagePort;
this.port2 = port2 as unknown as MessagePort;
this.port1 = port1;
this.port2 = port2;
}
}
@@ -88,7 +88,7 @@ function isError(message: Message): message is ErrorMessage {
* Calling methods on the switchboard causes messages to be sent through the channel.
*/
export class Switchboard {
port!: MessagePort;
port: MessagePort;
name = '';
@@ -97,9 +97,9 @@ export class Switchboard {
// used to make unique ids
incrementor = 1;
debugMode = false;
debugMode: boolean;
private isInitialised = false;
private isInitialised: boolean;
constructor(params?: Params) {
if (!params) {
@@ -56,7 +56,6 @@ import {
VisualMapComponent,
LegendComponent,
DataZoomComponent,
type DataZoomComponentOption,
ToolboxComponent,
GraphicComponent,
AriaComponent,
@@ -281,56 +280,12 @@ function Echart(
const notMerge = !isDashboardRefreshing;
chartRef.current?.dispatchAction({ type: 'hideTip' });
// setOption(notMerge:true) replaces the dataZoom config, dropping any
// range the user has engaged. Preserve it across the call.
const previousZoom = notMerge
? (chartRef.current?.getOption() as { dataZoom?: DataZoomComponentOption[] })
?.dataZoom
: undefined;
chartRef.current?.setOption(themedEchartOptions, {
notMerge,
replaceMerge: notMerge ? undefined : ['series'],
// lazyUpdate defers render, causing tooltip crashes on stale shapes (#39247)
lazyUpdate: false,
});
if (previousZoom?.length) {
// Skip restore when the new option reshapes dataZoom (different count
// means index-based restore could land on the wrong component).
const newZoom = (
chartRef.current?.getOption() as {
dataZoom?: DataZoomComponentOption[];
}
)?.dataZoom;
if (newZoom?.length === previousZoom.length) {
const batch = previousZoom
.map((dz, dataZoomIndex) => ({
dataZoomIndex,
start: dz.start,
end: dz.end,
startValue: dz.startValue,
endValue: dz.endValue,
}))
.filter(b => {
const hasAny =
b.start !== undefined ||
b.end !== undefined ||
b.startValue !== undefined ||
b.endValue !== undefined;
if (!hasAny) return false;
// Default full-range zoom is functionally identical to the
// fresh state setOption already produces — skip the dispatch.
const isDefaultRange =
b.start === 0 &&
b.end === 100 &&
b.startValue === undefined &&
b.endValue === undefined;
return !isDefaultRange;
});
if (batch.length) {
chartRef.current?.dispatchAction({ type: 'dataZoom', batch });
}
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- isDashboardRefreshing intentionally excluded to prevent extra setOption calls
}, [didMount, echartOptions, eventHandlers, zrEventHandlers, theme, vizType]);
@@ -1,192 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { EChartsCoreOption } from 'echarts/core';
import { render, waitFor } from 'spec/helpers/testing-library';
const setOption = jest.fn();
const on = jest.fn();
const off = jest.fn();
const resize = jest.fn();
const dispose = jest.fn();
const dispatchAction = jest.fn();
const getOption = jest.fn();
const mockInstance = {
setOption,
on,
off,
resize,
dispose,
dispatchAction,
getOption,
getZr: () => ({ on: jest.fn(), off: jest.fn() }),
};
jest.mock('echarts/core', () => ({
__esModule: true,
use: jest.fn(),
init: jest.fn(() => mockInstance),
registerLocale: jest.fn(),
}));
jest.mock('echarts/charts', () => ({}));
jest.mock('echarts/renderers', () => ({}));
jest.mock('echarts/components', () => ({}));
jest.mock('echarts/features', () => ({}));
// eslint-disable-next-line import/first
import Echart from '../../src/components/Echart';
const renderEchart = (echartOptions: EChartsCoreOption) => {
const refs = { divRef: undefined };
return render(
<Echart
width={400}
height={300}
echartOptions={echartOptions}
refs={refs}
/>,
{ useRedux: true, useTheme: true },
);
};
beforeEach(() => {
setOption.mockClear();
on.mockClear();
off.mockClear();
resize.mockClear();
dispatchAction.mockClear();
getOption.mockReset();
});
test('preserves user dataZoom range across setOption(notMerge)', async () => {
// After the user has zoomed, ECharts reports the current dataZoom range
// via getOption().dataZoom. We expect Echart to capture this before
// setOption replaces the option payload, then restore it via dispatchAction.
getOption.mockReturnValue({
dataZoom: [{ start: 12, end: 48 }],
});
const { rerender } = renderEchart({ xAxis: {}, series: [] });
// Trigger another setOption call by changing the echartOptions reference
rerender(
<Echart
width={400}
height={300}
echartOptions={{ xAxis: {}, series: [{ type: 'line' }] }}
refs={{ divRef: undefined }}
/>,
);
await waitFor(() =>
expect(dispatchAction).toHaveBeenCalledWith(
expect.objectContaining({
type: 'dataZoom',
batch: [
expect.objectContaining({ dataZoomIndex: 0, start: 12, end: 48 }),
],
}),
),
);
});
test('does not restore when no prior zoom range exists', async () => {
// Fresh chart with no engaged zoom: dataZoom config has no start/end.
getOption.mockReturnValue({
dataZoom: [{ type: 'slider', show: true }],
});
const { rerender } = renderEchart({ xAxis: {}, series: [] });
await waitFor(() => expect(setOption).toHaveBeenCalledTimes(1));
rerender(
<Echart
width={400}
height={300}
echartOptions={{ xAxis: {}, series: [{ type: 'line' }] }}
refs={{ divRef: undefined }}
/>,
);
await waitFor(() => expect(setOption).toHaveBeenCalledTimes(2));
const dataZoomCalls = dispatchAction.mock.calls.filter(
([action]) => action?.type === 'dataZoom',
);
expect(dataZoomCalls).toHaveLength(0);
});
test('does not restore when prior zoom is at default full range', async () => {
// ECharts populates start:0/end:100 on slider dataZoom by default, so
// every untouched timeseries would otherwise dispatch a redundant action
// on each re-render. Skip the dispatch when the range is just the default.
getOption.mockReturnValue({
dataZoom: [{ type: 'slider', show: true, start: 0, end: 100 }],
});
const { rerender } = renderEchart({ xAxis: {}, series: [] });
await waitFor(() => expect(setOption).toHaveBeenCalledTimes(1));
rerender(
<Echart
width={400}
height={300}
echartOptions={{ xAxis: {}, series: [{ type: 'line' }] }}
refs={{ divRef: undefined }}
/>,
);
await waitFor(() => expect(setOption).toHaveBeenCalledTimes(2));
const dataZoomCalls = dispatchAction.mock.calls.filter(
([action]) => action?.type === 'dataZoom',
);
expect(dataZoomCalls).toHaveLength(0);
});
test('does not restore when the new option reshapes dataZoom', async () => {
// 1st render starts with no engaged zoom; 2nd render captures an engaged
// range but the post-setOption dataZoom has a different count, so
// index-based restore could write to the wrong component. Skip in that case.
getOption
// 1st render: previousZoom + newZoom (no engaged values, nothing to dispatch)
.mockReturnValueOnce({ dataZoom: [{ type: 'slider' }] })
.mockReturnValueOnce({ dataZoom: [{ type: 'slider' }] })
// 2nd render: previousZoom has user range, but newZoom has 2 entries
.mockReturnValueOnce({ dataZoom: [{ start: 12, end: 48 }] })
.mockReturnValueOnce({
dataZoom: [{ start: 12, end: 48 }, { type: 'inside' }],
});
const { rerender } = renderEchart({ xAxis: {}, series: [] });
await waitFor(() => expect(setOption).toHaveBeenCalledTimes(1));
rerender(
<Echart
width={400}
height={300}
echartOptions={{ xAxis: {}, series: [{ type: 'line' }] }}
refs={{ divRef: undefined }}
/>,
);
await waitFor(() => expect(setOption).toHaveBeenCalledTimes(2));
const dataZoomCalls = dispatchAction.mock.calls.filter(
([action]) => action?.type === 'dataZoom',
);
expect(dataZoomCalls).toHaveLength(0);
});
@@ -27,9 +27,9 @@
],
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.24.0",
"mapbox-gl": "^3.23.1",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.1",
"react-map-gl": "^8.1.0",
"supercluster": "^8.0.1"
},
"peerDependencies": {
@@ -97,7 +97,7 @@ export function createWrapper(options?: Options) {
}
if (useDnd) {
// @ts-ignore react-dnd's DndProviderProps omits `children` under React 18 types
// @ts-expect-error react-dnd types not updated for React 18
result = <DndProvider backend={HTML5Backend}>{result}</DndProvider>;
}
+6 -12
View File
@@ -1671,7 +1671,7 @@ export interface VizOptions {
export function createDatasource(
vizOptions: VizOptions,
): SqlLabThunkAction<Promise<{ id: number }>> {
): SqlLabThunkAction<Promise<unknown>> {
return (dispatch: AppDispatch) => {
dispatch(createDatasourceStarted());
const { dbId, catalog, schema, datasourceName, sql, templateParams } =
@@ -1691,10 +1691,9 @@ export function createDatasource(
}),
})
.then(({ json }) => {
const result = json as { id: number };
dispatch(createDatasourceSuccess(result));
dispatch(createDatasourceSuccess(json as { id: number }));
return result;
return Promise.resolve(json);
})
.catch(error => {
getClientErrorObject(error).then(e => {
@@ -1713,7 +1712,7 @@ export function createDatasource(
export function createCtasDatasource(
vizOptions: Record<string, unknown>,
): SqlLabThunkAction<Promise<{ table_id: number }>> {
): SqlLabThunkAction<Promise<{ id: number }>> {
return (dispatch: AppDispatch) => {
dispatch(createDatasourceStarted());
return SupersetClient.post({
@@ -1721,14 +1720,9 @@ export function createCtasDatasource(
jsonPayload: vizOptions,
})
.then(({ json }) => {
const result = json.result as { table_id: number };
// The endpoint's `result.table_id` IS the dataset id; normalize so
// createDatasourceSuccess's `${data.id}__table` resolves correctly.
// Without this, the CTAS Explore button silently produced
// `"undefined__table"` because `result.id` doesn't exist.
dispatch(createDatasourceSuccess({ id: result.table_id }));
dispatch(createDatasourceSuccess(json.result));
return result;
return json.result;
})
.catch(() => {
const errorMsg = t('An error occurred while creating the data source');
@@ -19,8 +19,7 @@
import { useRef, useEffect, FC, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useDispatch, useSelector } from 'react-redux';
import { logging } from '@apache-superset/core/utils';
import {
SqlLabRootState,
@@ -87,7 +86,7 @@ const EditorAutoSync: FC = () => {
const editorTabLastUpdatedAt = useSelector<SqlLabRootState, number>(
state => state.sqlLab.editorTabLastUpdatedAt,
);
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const lastSavedTimestampRef = useRef<number>(editorTabLastUpdatedAt);
const currentQueryEditorId = useSelector<SqlLabRootState, string>(
@@ -17,8 +17,7 @@
* under the License.
*/
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { usePrevious } from '@superset-ui/core';
import { css, useTheme } from '@apache-superset/core/theme';
import { Global } from '@emotion/react';
@@ -137,7 +136,7 @@ const EditorWrapper = ({
height,
hotkeys,
}: EditorWrapperProps) => {
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const queryEditor = useQueryEditor(queryEditorId, [
'id',
'dbId',
@@ -17,8 +17,7 @@
* under the License.
*/
import { useEffect, useMemo, useRef } from 'react';
import { useStore } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useDispatch, useStore } from 'react-redux';
import { t } from '@apache-superset/core/translation';
import { getExtensionsRegistry } from '@superset-ui/core';
@@ -69,7 +68,7 @@ export function useKeywords(
catalog,
schema,
});
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const hasFetchedKeywords = useRef(false);
// skipFetch is used to prevent re-evaluating memoized keywords
// due to updated api results by skip flag
@@ -16,10 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useSelector, useDispatch } from 'react-redux';
import { t } from '@apache-superset/core/translation';
import { VizType } from '@superset-ui/core';
import { JsonObject, VizType } from '@superset-ui/core';
import {
createCtasDatasource,
addInfoToast,
@@ -46,7 +45,7 @@ const ExploreCtasResultsButton = ({
const errorMessage = useSelector(
(state: SqlLabRootState) => state.sqlLab.errorMessage,
);
const dispatch = useAppDispatch();
const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
const buildVizOptions = {
table_name: table,
@@ -57,7 +56,7 @@ const ExploreCtasResultsButton = ({
const visualize = () => {
dispatch(createCtasDatasource(buildVizOptions))
.then(data => {
.then((data: { table_id: number }) => {
const formData = {
datasource: `${data.table_id}__table`,
metrics: ['count'],
@@ -17,8 +17,7 @@
* under the License.
*/
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useDispatch, useSelector } from 'react-redux';
import URI from 'urijs';
import { pick } from 'lodash';
import { useComponentDidUpdate } from '@superset-ui/core';
@@ -50,7 +49,7 @@ const PopEditorTab: React.FC<{ children?: React.ReactNode }> = ({
({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0],
);
const [updatedUrl, setUpdatedUrl] = useState<string>(SQL_LAB_URL);
const dispatch = useAppDispatch();
const dispatch = useDispatch();
useComponentDidUpdate(() => {
setQueryEditorId(assigned => assigned ?? activeQueryEditorId);
if (activeQueryEditorId) {
@@ -17,8 +17,7 @@
* under the License.
*/
import { useRef } from 'react';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useSelector, useDispatch } from 'react-redux';
import { isObject } from 'lodash';
import rison from 'rison';
import {
@@ -83,7 +82,7 @@ function QueryAutoRefresh({
.map(({ id }) => id),
),
);
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const checkForRefresh = () => {
const shouldRequestChecking = shouldCheckForQueries(queries);
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useAppDispatch } from 'src/views/store';
import { useDispatch } from 'react-redux';
import { t } from '@apache-superset/core/translation';
import { Dropdown, Button } from '@superset-ui/core/components';
import { Menu } from '@superset-ui/core/components/Menu';
@@ -75,7 +75,7 @@ const QueryLimitSelect = ({
maxRow,
defaultQueryLimit,
}: QueryLimitSelectProps) => {
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const queryEditor = useQueryEditor(queryEditorId, ['id', 'queryLimit']);
const queryLimit = queryEditor.queryLimit || defaultQueryLimit;
@@ -30,8 +30,7 @@ import ProgressBar from '@superset-ui/core/components/ProgressBar';
import { t } from '@apache-superset/core/translation';
import { QueryResponse, QueryState } from '@superset-ui/core';
import { useTheme } from '@apache-superset/core/theme';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import {
queryEditorSetSql,
@@ -93,7 +92,7 @@ const QueryTable = ({
latestQueryId,
}: QueryTableProps) => {
const theme = useTheme();
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const [selectedQuery, setSelectedQuery] = useState<QueryResponse | null>(
null,
);
@@ -27,8 +27,7 @@ import {
} from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { pick } from 'lodash';
import {
@@ -232,7 +231,7 @@ const ResultSet = ({
canCopyClipboardSqlLab: canCopyClipboard,
} = usePermissions();
const history = useHistory();
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const logAction = useLogAction({ queryId, sqlEditorId: query.sqlEditorId });
const { showConfirm, ConfirmModal } = useConfirmModal();
@@ -16,7 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { act, type ComponentProps } from 'react';
import * as reactRedux from 'react-redux';
import { act } from 'react';
import {
cleanup,
fireEvent,
@@ -39,19 +40,6 @@ const mockedProps = {
datasource: testQuery,
};
// Render with the SqlLab user fixture preloaded into the mock store so the
// component's useSelector(state => state.user) returns a useful value.
// Previously this test used jest.spyOn(reactRedux, 'useSelector') to inject
// the user directly, which can't intercept calls routed through the typed
// useAppSelector hook.
const renderModal = (
props: Partial<ComponentProps<typeof SaveDatasetModal>> = {},
) =>
render(<SaveDatasetModal {...mockedProps} {...props} />, {
useRedux: true,
initialState: { user },
});
fetchMock.get('glob:*/api/v1/dataset/?*', {
result: mockdatasets,
dataset_count: 3,
@@ -59,17 +47,17 @@ fetchMock.get('glob:*/api/v1/dataset/?*', {
jest.useFakeTimers({ advanceTimers: true });
// Mock the user
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
beforeEach(() => {
useSelectorMock.mockClear();
cleanup();
});
// Mock createDatasource to return a thunk that resolves with the dataset's
// new id. The test's mock store includes redux-thunk middleware (from RTK's
// getDefaultMiddleware), so dispatch(createDatasource(...)) properly unwraps
// the thunk and the production code's .then((data) => clearDatasetCache(data.id))
// chain receives `{ id: 123 }`. Individual tests can override per-call as needed.
// Mock the createDatasource action
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
jest.mock('src/SqlLab/actions/sqlLab', () => ({
createDatasource: jest.fn(() => () => Promise.resolve({ id: 123 })),
createDatasource: jest.fn(),
}));
jest.mock('src/explore/exploreUtils/formData', () => ({
postFormData: jest.fn(),
@@ -82,7 +70,7 @@ jest.mock('src/utils/cachedSupersetGet', () => ({
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SaveDatasetModal', () => {
test('renders a "Save as new" field', () => {
renderModal();
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const saveRadioBtn = screen.getByRole('radio', {
name: /save as new/i,
@@ -99,7 +87,7 @@ describe('SaveDatasetModal', () => {
});
test('renders an "Overwrite existing" field', () => {
renderModal();
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const overwriteRadioBtn = screen.getByRole('radio', {
name: /overwrite existing/i,
@@ -115,20 +103,20 @@ describe('SaveDatasetModal', () => {
});
test('renders a close button', () => {
renderModal();
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
});
test('renders a save button when "Save as new" is selected', () => {
renderModal();
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// "Save as new" is selected when the modal opens by default
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});
test('renders an overwrite button when "Overwrite existing" is selected', () => {
renderModal();
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// Click the overwrite radio button to reveal the overwrite confirmation and back buttons
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -142,7 +130,8 @@ describe('SaveDatasetModal', () => {
});
test('renders the overwrite button as disabled until an existing dataset is selected', async () => {
renderModal();
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// Click the overwrite radio button
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -179,7 +168,8 @@ describe('SaveDatasetModal', () => {
});
test('renders a confirm overwrite screen when overwrite is clicked', async () => {
renderModal();
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// Click the overwrite radio button
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -225,7 +215,11 @@ describe('SaveDatasetModal', () => {
});
test('sends the schema when creating the dataset', async () => {
renderModal();
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -246,9 +240,17 @@ describe('SaveDatasetModal', () => {
});
test('sends the catalog when creating the dataset', async () => {
renderModal({
datasource: { ...mockedProps.datasource, catalog: 'public' },
});
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
render(
<SaveDatasetModal
{...mockedProps}
datasource={{ ...mockedProps.datasource, catalog: 'public' }}
/>,
{ useRedux: true },
);
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -269,7 +271,7 @@ describe('SaveDatasetModal', () => {
});
test('does not renders a checkbox button when template processing is disabled', () => {
renderModal();
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
});
@@ -278,7 +280,7 @@ describe('SaveDatasetModal', () => {
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
};
renderModal();
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
expect(screen.getByRole('checkbox')).toBeInTheDocument();
});
@@ -287,11 +289,15 @@ describe('SaveDatasetModal', () => {
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
};
renderModal({
const propsWithTemplateParam = {
...mockedProps,
datasource: {
...testQuery,
templateParams: JSON.stringify({ my_param: 12 }),
},
};
render(<SaveDatasetModal {...propsWithTemplateParam} />, {
useRedux: true,
});
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -318,11 +324,15 @@ describe('SaveDatasetModal', () => {
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
};
renderModal({
const propsWithTemplateParam = {
...mockedProps,
datasource: {
...testQuery,
templateParams: JSON.stringify({ my_param: 12 }),
},
};
render(<SaveDatasetModal {...propsWithTemplateParam} />, {
useRedux: true,
});
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -383,11 +393,19 @@ describe('SaveDatasetModal', () => {
.spyOn(SupersetClient, 'put')
.mockResolvedValue({ json: { result: { id: 0 } } } as any);
renderModal({
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
const propsWithTemplateParam = {
...mockedProps,
datasource: {
...testQuery,
templateParams: JSON.stringify({ my_param: 12, _filters: 'foo' }),
},
};
render(<SaveDatasetModal {...propsWithTemplateParam} />, {
useRedux: true,
});
// Check the "Include Template Parameters" checkbox
@@ -425,11 +443,19 @@ describe('SaveDatasetModal', () => {
.spyOn(SupersetClient, 'put')
.mockResolvedValue({ json: { result: { id: 0 } } } as any);
renderModal({
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
const propsWithTemplateParam = {
...mockedProps,
datasource: {
...testQuery,
templateParams: JSON.stringify({ my_param: 12 }),
},
};
render(<SaveDatasetModal {...propsWithTemplateParam} />, {
useRedux: true,
});
// Do NOT check the "Include Template Parameters" checkbox
@@ -463,9 +489,12 @@ describe('SaveDatasetModal', () => {
'postFormData',
);
const dummyDispatch = jest.fn().mockResolvedValue({ id: 123 });
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
postFormData.mockResolvedValue('chart_key_123');
renderModal();
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -34,6 +34,7 @@ import { t } from '@apache-superset/core/translation';
import {
SupersetClient,
JsonResponse,
JsonObject,
QueryResponse,
QueryFormData,
VizType,
@@ -43,14 +44,16 @@ import {
} from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import { useAppDispatch, useAppSelector } from 'src/views/store';
import { useSelector, useDispatch } from 'react-redux';
import rison from 'rison';
import { createDatasource } from 'src/SqlLab/actions/sqlLab';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
import {
DatasetRadioState,
EXPLORE_CHART_DEFAULT,
DatasetOwner,
SqlLabRootState,
} from 'src/SqlLab/types';
import { mountExploreUrl } from 'src/explore/exploreUtils';
import { postFormData } from 'src/explore/exploreUtils/formData';
@@ -218,7 +221,7 @@ export const SaveDatasetModal = ({
openWindow = true,
formData = {},
}: SaveDatasetModalProps) => {
const defaultVizType = useAppSelector(
const defaultVizType = useSelector<SqlLabRootState, string>(
state => state.common?.conf?.DEFAULT_VIZ_TYPE || VizType.Table,
);
@@ -237,8 +240,8 @@ export const SaveDatasetModal = ({
>(undefined);
const [loading, setLoading] = useState<boolean>(false);
const user = useAppSelector(state => state.user);
const dispatch = useAppDispatch();
const user = useSelector<SqlLabRootState, User>(state => state.user);
const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
const [includeTemplateParameters, setIncludeTemplateParameters] =
useState(false);
@@ -17,8 +17,7 @@
* under the License.
*/
import { createRef, useCallback, useMemo } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { nanoid } from 'nanoid';
import Tabs from '@superset-ui/core/components/Tabs';
import { t } from '@apache-superset/core/translation';
@@ -106,7 +105,7 @@ const SouthPane = ({
const { id, tabViewId } = useQueryEditor(queryEditorId, ['tabViewId']);
const editorId = tabViewId ?? id;
const theme = useTheme();
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const viewItems = views.getViews(ViewLocations.sqllab.panels) || [];
const { offline, tables } = useSelector(
({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
@@ -30,8 +30,7 @@ import {
import type { editors } from '@apache-superset/core';
import useEffectEvent from 'src/hooks/useEffectEvent';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import AutoSizer from 'react-virtualized-auto-sizer';
import { t } from '@apache-superset/core/translation';
import {
@@ -238,7 +237,7 @@ const SqlEditor: FC<Props> = ({
scheduleQueryWarning,
}) => {
const theme = useTheme();
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const { database, latestQuery, currentQueryEditorId, hasSqlStatement } =
useSelector<
@@ -17,7 +17,7 @@
* under the License.
*/
import { useCallback, useState } from 'react';
import { useAppDispatch } from 'src/views/store';
import { useDispatch } from 'react-redux';
import { resetState } from 'src/SqlLab/actions/sqlLab';
import {
@@ -69,7 +69,7 @@ const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
const { db, catalog, schema, onDbChange, onCatalogChange, onSchemaChange } =
dbSelectorProps;
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const shouldShowReset = window.location.search === '?reset=1';
// Modal state for Database/Catalog/Schema selector
@@ -19,8 +19,7 @@
import { useMemo, FC } from 'react';
import { bindActionCreators } from 'redux';
import { useSelector, shallowEqual } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { MenuDotsDropdown } from '@superset-ui/core/components';
import { Menu, MenuItemType } from '@superset-ui/core/components/Menu';
import { t } from '@apache-superset/core/translation';
@@ -91,7 +90,7 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
);
const StatusIcon = queryState ? STATE_ICONS[queryState] : STATE_ICONS.running;
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const actions = useMemo(
() =>
bindActionCreators(
@@ -17,8 +17,7 @@
* under the License.
*/
import { useEffect, useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useDispatch, useSelector } from 'react-redux';
import { SqlLabRootState } from 'src/SqlLab/types';
import {
@@ -42,7 +41,7 @@ export default function useDatabaseSelector(queryEditorId: string) {
SqlLabRootState,
SqlLabRootState['sqlLab']['databases']
>(({ sqlLab }) => sqlLab.databases);
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const queryEditor = useQueryEditor(queryEditorId, [
'dbId',
'catalog',
@@ -17,8 +17,7 @@
* under the License.
*/
import { useState, useRef, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useDispatch, useSelector } from 'react-redux';
import type { QueryEditor, SqlLabRootState, Table } from 'src/SqlLab/types';
import {
ButtonGroup,
@@ -76,7 +75,7 @@ const Fade = styled.div`
const TableElement = ({ table, ...props }: TableElementProps) => {
const { dbId, catalog, schema, name, expanded, id } = table;
const theme = useTheme();
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const {
currentData: tableMetadata,
isSuccess: isMetadataSuccess,
@@ -25,8 +25,7 @@ import {
type ChangeEvent,
useMemo,
} from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { styled, css, useTheme } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -164,7 +163,7 @@ const savePinnedSchemasToStorage = (
};
const TableExploreTree: React.FC<Props> = ({ queryEditorId }) => {
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const theme = useTheme();
const treeRef = useRef<TreeApi<TreeNodeData>>(null);
const tables = useSelector(
@@ -17,7 +17,7 @@
* under the License.
*/
import { useMemo, useReducer, useCallback } from 'react';
import { useAppDispatch } from 'src/views/store';
import { useDispatch } from 'react-redux';
import { t } from '@apache-superset/core/translation';
import {
Table,
@@ -130,7 +130,7 @@ const useTreeData = ({
catalog,
pinnedTables,
}: UseTreeDataParams): UseTreeDataResult => {
const reduxDispatch = useAppDispatch();
const reduxDispatch = useDispatch();
// Schema data from API
const {
currentData: schemaData,
@@ -17,8 +17,7 @@
* under the License.
*/
import { type FC, useCallback, useMemo, useRef, useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { nanoid } from 'nanoid';
import { t } from '@apache-superset/core/translation';
import { ClientErrorObject, getExtensionsRegistry } from '@superset-ui/core';
@@ -111,7 +110,7 @@ const renderWell = (partitions: TableMetaData['partitions']) => {
};
const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
const dispatch = useAppDispatch();
const dispatch = useDispatch();
const theme = useTheme();
const [databaseName, backend, disableDataPreview] = useSelector<
SqlLabRootState,
@@ -28,7 +28,7 @@ import fetchMock from 'fetch-mock';
import { SupersetClient } from '@superset-ui/core';
import mockDatasource from 'spec/fixtures/mockDatasource';
import React from 'react';
import DatasourceModalComponent, { buildExtraJsonObject } from '.';
import DatasourceModalComponent from '.';
// Cast to accept partial mock props in tests
const DatasourceModal = DatasourceModalComponent as unknown as React.FC<
@@ -309,35 +309,3 @@ describe('DatasourceModal', () => {
});
});
});
describe('buildExtraJsonObject', () => {
test('returns "{}" for an item with no warning and no certification', () => {
expect(buildExtraJsonObject({} as any)).toBe('{}');
});
test('drops warning_markdown when its value is null', () => {
expect(buildExtraJsonObject({ warning_markdown: null } as any)).toBe('{}');
});
test('drops warning_markdown when its value is an empty string', () => {
expect(buildExtraJsonObject({ warning_markdown: '' } as any)).toBe('{}');
});
test('preserves a non-empty warning_markdown verbatim', () => {
expect(buildExtraJsonObject({ warning_markdown: '⚠ caveat' } as any)).toBe(
'{"warning_markdown":"⚠ caveat"}',
);
});
test('preserves certification and drops null warning_markdown', () => {
expect(
buildExtraJsonObject({
certified_by: 'data-team',
certification_details: 'verified',
warning_markdown: null,
} as any),
).toBe(
'{"certification":{"certified_by":"data-team","details":"verified"}}',
);
});
});
@@ -71,7 +71,7 @@ const StyledDatasourceModal = styled(Modal)`
}
`;
export function buildExtraJsonObject(
function buildExtraJsonObject(
item: DatasetObject['metrics'][0] | DatasetObject['columns'][0],
) {
const certification =
@@ -83,7 +83,7 @@ export function buildExtraJsonObject(
: undefined;
return JSON.stringify({
certification,
warning_markdown: item?.warning_markdown || undefined,
warning_markdown: item?.warning_markdown,
});
}
@@ -1,267 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createRef } from 'react';
import {
render,
screen,
selectOption,
waitFor,
} from 'spec/helpers/testing-library';
import { ListViewFilterOperator } from '../types';
import UIFilters from './index';
import SelectFilter from './Select';
import type { FilterHandler } from './types';
const mockUpdateFilterValue = jest.fn();
beforeEach(() => {
mockUpdateFilterValue.mockClear();
});
test('select filter with ReactNode label uses option title when serializing selection', async () => {
// Regression for sc-104554: the chart-list Owner filter renders options
// with ReactNode labels (name + email). The value passed to
// updateFilterValue is serialized into URL / filter state and re-used to
// render the filter pill on return. It must carry the plain-text name
// (from `title`) and not fall back to the numeric user id.
const ReactNodeLabel = (
<div>
<span>John Doe</span>
<span>john@example.com</span>
</div>
);
const fetchSelects = jest.fn().mockResolvedValue({
data: [
{
label: ReactNodeLabel,
value: 42,
title: 'John Doe',
},
],
totalCount: 1,
});
const filters = [
{
Header: 'Owner',
key: 'owner',
id: 'owners',
input: 'select' as const,
operator: ListViewFilterOperator.RelationManyMany,
unfilteredLabel: 'All',
fetchSelects,
paginate: true,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await selectOption('John Doe', 'Owner');
await waitFor(() => {
expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
label: 'John Doe',
value: 42,
});
});
});
test('select filter falls back to stringified value when no string label or title is available', async () => {
const fetchSelects = jest.fn().mockResolvedValue({
data: [
{
label: <span>123</span>,
value: 123,
},
],
totalCount: 1,
});
const filters = [
{
Header: 'Something',
key: 'something',
id: 'something',
input: 'select' as const,
operator: ListViewFilterOperator.RelationOneMany,
unfilteredLabel: 'All',
fetchSelects,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await selectOption('123', 'Something');
await waitFor(() => {
expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
label: '123',
value: 123,
});
});
});
test('plain select with string label passes label through unchanged', async () => {
// Happy-path coverage for the typeof-string branch in onChange, exercised
// through the non-async Select wrapper (selects array, no fetchSelects).
const filters = [
{
Header: 'Status',
key: 'status',
id: 'status',
input: 'select' as const,
operator: ListViewFilterOperator.Equals,
unfilteredLabel: 'All',
selects: [
{ label: 'Published', value: 7 },
{ label: 'Draft', value: 8 },
],
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await selectOption('Published', 'Status');
await waitFor(() => {
expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
label: 'Published',
value: 7,
});
});
});
test('plain select with ReactNode label uses option title when serializing selection', async () => {
// Parallel coverage to the AsyncSelect ReactNode-with-title test, against
// the non-async Select wrapper. Guards against the two wrappers ever
// diverging on antd's two-arg onChange shape.
const ReactNodeLabel = (
<div>
<span>Jane Roe</span>
<span>jane@example.com</span>
</div>
);
const filters = [
{
Header: 'Owner',
key: 'owner',
id: 'owners',
input: 'select' as const,
operator: ListViewFilterOperator.RelationManyMany,
unfilteredLabel: 'All',
selects: [{ label: ReactNodeLabel, value: 99, title: 'Jane Roe' }],
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await selectOption('Jane Roe', 'Owner');
await waitFor(() => {
expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
label: 'Jane Roe',
value: 99,
});
});
});
test('clearFilter notifies onSelect with undefined and isClear=true', () => {
// The isClear flag is what allows the parent (Filters/index) to suppress
// onFilterUpdate side-effects when the user clears the filter rather than
// picking a new value. Lock that contract in.
const mockOnSelect = jest.fn();
const ref = createRef<FilterHandler>();
render(
<SelectFilter
Header="Owner"
initialValue={{ label: 'John Doe', value: 42 }}
onSelect={mockOnSelect}
selects={[{ label: 'John Doe', value: 42, title: 'John Doe' }]}
ref={ref}
/>,
);
ref.current?.clearFilter();
expect(mockOnSelect).toHaveBeenCalledWith(undefined, true);
});
test('rehydrates filter pill from initialValue with plain-string label', async () => {
// The user-visible regression: after URL/state rehydration the filter pill
// must render the human-readable name, not the numeric user id. The fix
// ensures the persisted label is a string; this test asserts that string
// is what surfaces in the rendered combobox selection.
const filters = [
{
Header: 'Owner',
key: 'owner',
id: 'owners',
input: 'select' as const,
operator: ListViewFilterOperator.RelationManyMany,
unfilteredLabel: 'All',
fetchSelects: jest.fn().mockResolvedValue({ data: [], totalCount: 0 }),
paginate: true,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[
{
id: 'owners',
operator: ListViewFilterOperator.RelationManyMany,
value: { label: 'John Doe', value: 42 },
},
]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
@@ -58,22 +58,14 @@ function SelectFilter(
) {
const [selectedOption, setSelectedOption] = useState(initialValue);
const onChange = (selected: SelectOption, option?: SelectOption) => {
// antd's `onChange` (with `labelInValue`) passes the `{label, value}`
// labeled-value as the first arg and the full option (which carries
// `title` and any other fields) as the second. Options may supply a
// ReactNode label (e.g. OwnerSelectLabel for the chart list Owner
// filter). Since this object is serialized into the URL and rehydrated
// as the filter pill on return, we need a plain string. Prefer `title`
// (set by callers to the human-readable name) before falling back to
// the value.
const onChange = (selected: SelectOption) => {
onSelect(
selected
? {
label:
typeof selected.label === 'string'
? selected.label
: (option?.title ?? String(selected.value)),
: String(selected.value),
value: selected.value,
}
: undefined,
@@ -26,10 +26,6 @@ export interface SortColumn {
export interface SelectOption {
label: ReactNode;
value: any;
// Plain-text representation of the option. Callers should set this when
// `label` is a ReactNode so that the option can be serialized (e.g. into
// URL filter state) without losing the human-readable name.
title?: string;
[key: string]: unknown;
}
@@ -25,7 +25,7 @@ import {
SupersetClient,
} from '@superset-ui/core';
import { MenuItem } from '@superset-ui/core/components/Menu';
import { parse as parseContentDisposition } from 'content-disposition';
import contentDisposition from 'content-disposition';
import { useDownloadScreenshot } from 'src/dashboard/hooks/useDownloadScreenshot';
import { MenuKeys } from 'src/dashboard/types';
import downloadAsPdf from 'src/utils/downloadAsPdf';
@@ -122,7 +122,7 @@ export const useDownloadMenuItems = (
if (disposition) {
try {
const parsed = parseContentDisposition(disposition);
const parsed = contentDisposition.parse(disposition);
if (parsed?.parameters?.filename) {
fileName = parsed.parameters.filename;
}
@@ -21,7 +21,7 @@ import { useSelector } from 'react-redux';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { last } from 'lodash';
import rison from 'rison';
import { parse as parseContentDisposition } from 'content-disposition';
import contentDisposition from 'content-disposition';
import { t } from '@apache-superset/core/translation';
import { SupersetClient, SupersetApiError } from '@superset-ui/core';
import { logging } from '@apache-superset/core/utils';
@@ -105,7 +105,7 @@ export const useDownloadScreenshot = (
if (disposition) {
try {
const parsed = parseContentDisposition(disposition);
const parsed = contentDisposition.parse(disposition);
if (parsed?.parameters?.filename) {
fileName = parsed.parameters.filename;
}
@@ -103,12 +103,10 @@ test('does not allow user to create a report without a name', () => {
});
test('creates a new email report via modal Add button', async () => {
// The modal now calls POST /api/v1/report/subscribe; creation_method, owners, and
// recipients are derived server-side — the client payload intentionally omits them.
fetchMock.post(
'glob:*/api/v1/report/subscribe',
REPORT_ENDPOINT,
{ id: 1, result: {} },
{ name: 'post-subscribe' },
{ name: 'post-report' },
);
render(<ReportModal {...defaultProps} />, { useRedux: true });
@@ -116,22 +114,22 @@ test('creates a new email report via modal Add button', async () => {
const addButton = screen.getByRole('button', { name: /add/i });
await waitFor(() => userEvent.click(addButton));
// Verify exactly one POST to the subscribe endpoint
// Verify exactly one POST from the modal submit path
await waitFor(() => {
const postCalls = fetchMock.callHistory.calls('post-subscribe');
const postCalls = fetchMock.callHistory.calls('post-report');
expect(postCalls).toHaveLength(1);
});
const postCalls = fetchMock.callHistory.calls('post-subscribe');
const postCalls = fetchMock.callHistory.calls('post-report');
const body = JSON.parse(postCalls[0].options.body as string);
expect(body.name).toBe('Weekly Report');
expect(body.type).toBe('Report');
expect(body.creation_method).toBe('dashboards');
expect(body.crontab).toBeDefined();
// creation_method, owners, and recipients are set server-side; not in the client payload
expect(body.creation_method).toBeUndefined();
expect(body.recipients).toBeUndefined();
expect(body.recipients).toBeDefined();
expect(body.recipients[0].type).toBe('Email');
fetchMock.removeRoute('post-subscribe');
fetchMock.removeRoute('post-report');
});
test('text-based chart hides screenshot width and shows message content', () => {
@@ -174,28 +174,6 @@ export const addReport =
throw err;
});
export const SUBSCRIBE_REPORT = 'SUBSCRIBE_REPORT' as const;
export interface SubscribeReportAction {
type: typeof SUBSCRIBE_REPORT;
json: ReportApiJsonResponse;
}
export const subscribeReport =
(report: Partial<ReportObject>) => (dispatch: Dispatch<AnyAction>) =>
SupersetClient.post({
endpoint: `/api/v1/report/subscribe`,
jsonPayload: report,
})
.then(({ json }) => {
dispatch({ type: SUBSCRIBE_REPORT, json } as SubscribeReportAction);
dispatch(addSuccessToast(t('The report has been created')));
})
.catch(err => {
dispatch(addDangerToast(t('Failed to create report')));
throw err;
});
export const EDIT_REPORT = 'EDIT_REPORT' as const;
export interface EditReportAction {
@@ -277,6 +255,5 @@ export function deleteActiveReport(report: DeletableReport) {
export type ReportAction =
| SetReportAction
| AddReportAction
| SubscribeReportAction
| EditReportAction
| DeleteReportAction;
@@ -31,8 +31,8 @@ import { Alert } from '@apache-superset/core/components';
import { SupersetTheme } from '@apache-superset/core/theme';
import { useDispatch, useSelector } from 'react-redux';
import {
addReport,
editReport,
subscribeReport,
} from 'src/features/reports/ReportModal/actions';
import {
Input,
@@ -179,13 +179,26 @@ function ReportModal({
}, [isEditMode, report]);
const onSave = async () => {
const commonFields: Partial<ReportObject> = {
// Create new Report
const newReportValues: Partial<ReportObject> = {
type: 'Report',
active: true,
force_screenshot: false,
custom_width: currentReport.custom_width,
creation_method: creationMethod,
dashboard: dashboardId,
chart: chart?.id,
owners: [userId],
recipients: [
{
recipient_config_json: {
target: userEmail,
ccTarget: ccEmail,
bccTarget: bccEmail,
},
type: 'Email',
},
],
name: currentReport.name,
description: currentReport.description,
crontab: currentReport.crontab,
@@ -196,27 +209,12 @@ function ReportModal({
setCurrentReport({ isSubmitting: true, error: undefined });
try {
if (isEditMode && currentReport.id) {
// Edit path: include all fields, PUT endpoint accepts recipients/owners directly
await dispatch(
editReport(currentReport.id, {
...commonFields,
creation_method: creationMethod,
owners: [userId],
recipients: [
{
recipient_config_json: {
target: userEmail,
ccTarget: ccEmail,
bccTarget: bccEmail,
},
type: 'Email',
},
],
} as ReportObject),
editReport(currentReport.id, newReportValues as ReportObject),
);
} else {
// Subscribe path: creation_method, owners, and recipients are set server-side.
await dispatch(subscribeReport(commonFields as ReportObject));
// Create new report (either not in edit mode, or edit mode without valid ID)
await dispatch(addReport(newReportValues as ReportObject));
}
onHide();
} catch (e) {
@@ -21,13 +21,11 @@ import { omit } from 'lodash';
import {
SET_REPORT,
ADD_REPORT,
SUBSCRIBE_REPORT,
EDIT_REPORT,
DELETE_REPORT,
ReportAction,
SetReportAction,
AddReportAction,
SubscribeReportAction,
EditReportAction,
DeleteReportAction,
} from './actions';
@@ -107,25 +105,6 @@ export default function reportsReducer(
};
},
[SUBSCRIBE_REPORT]() {
const { result, id } = (action as SubscribeReportAction).json;
const report: ReportObject = { ...result, id } as ReportObject;
const creationMethod = report.creation_method as ReportCreationMethod;
const key = report.dashboard ?? report.chart;
if (key === undefined) {
return state;
}
return {
...state,
[creationMethod]: {
...state[creationMethod],
[key]: report,
},
};
},
[EDIT_REPORT]() {
const actionTyped = action as EditReportAction;
const report: ReportObject = {
@@ -117,7 +117,6 @@ type LaunchQueue = {
const pendingTimerIds = new Set<ReturnType<typeof setTimeout>>();
const MAX_CONSUMER_POLL_ATTEMPTS = 50;
const consumerPromises: Promise<void>[] = [];
// Defer the consumer call to a macrotask so it doesn't fire synchronously inside
// the component's useEffect — calling it inline deadlocks Jest because the
@@ -132,11 +131,7 @@ const setupLaunchQueue = (fileHandle: MockFileHandle | null = null) => {
if (fileHandle) {
const id = setTimeout(() => {
pendingTimerIds.delete(id);
consumerPromises.push(
Promise.resolve(consumer({ files: [fileHandle] })).then(
() => undefined,
),
);
consumer({ files: [fileHandle] });
}, 0);
pendingTimerIds.add(id);
}
@@ -170,19 +165,9 @@ beforeEach(() => {
.launchQueue;
});
afterEach(async () => {
afterEach(() => {
pendingTimerIds.forEach(id => clearTimeout(id));
pendingTimerIds.clear();
if (consumerPromises.length > 0) {
const results = await Promise.allSettled(consumerPromises);
results.forEach(r => {
if (r.status === 'rejected') {
// eslint-disable-next-line no-console
console.warn('LaunchQueue consumer rejected:', r.reason);
}
});
consumerPromises.length = 0;
}
delete (window as unknown as Window & { launchQueue?: LaunchQueue })
.launchQueue;
});
+5 -8
View File
@@ -18,7 +18,7 @@
*/
import { SupersetClient } from '@superset-ui/core';
import { logging } from '@apache-superset/core/utils';
import { parse as parseContentDisposition } from 'content-disposition';
import contentDisposition from 'content-disposition';
import handleResourceExport from './export';
// Mock dependencies
@@ -35,10 +35,7 @@ jest.mock('@apache-superset/core/utils', () => ({
},
}));
jest.mock('content-disposition', () => ({
parse: jest.fn(),
__esModule: true,
}));
jest.mock('content-disposition');
// Default no-op mock for pathUtils; specific tests customize ensureAppRoot to simulate app root prefixing
jest.mock('./pathUtils', () => ({
@@ -159,7 +156,7 @@ test('uses default filename when Content-Disposition is missing', async () => {
});
test('handles Content-Disposition parsing errors gracefully', async () => {
(parseContentDisposition as jest.Mock).mockImplementationOnce(() => {
(contentDisposition.parse as jest.Mock).mockImplementationOnce(() => {
throw new Error('Invalid header');
});
@@ -211,7 +208,7 @@ test('exports multiple resources with correct IDs', async () => {
});
test('parses filename from Content-Disposition with quotes', async () => {
(parseContentDisposition as jest.Mock).mockReturnValueOnce({
(contentDisposition.parse as jest.Mock).mockReturnValueOnce({
type: 'attachment',
parameters: { filename: 'my_custom_export.zip' },
});
@@ -363,7 +360,7 @@ test('handles malformed Content-Disposition header', async () => {
} as unknown as Response;
(SupersetClient.get as jest.Mock).mockResolvedValue(mockResponse);
(parseContentDisposition as jest.Mock).mockImplementationOnce(() => {
(contentDisposition.parse as jest.Mock).mockImplementationOnce(() => {
throw new Error('Parse error');
});
+2 -2
View File
@@ -19,7 +19,7 @@
import { SupersetClient } from '@superset-ui/core';
import { logging } from '@apache-superset/core/utils';
import rison from 'rison';
import { parse as parseContentDisposition } from 'content-disposition';
import contentDisposition from 'content-disposition';
// Maximum blob size for in-memory downloads (100MB)
const MAX_BLOB_SIZE = 100 * 1024 * 1024;
@@ -75,7 +75,7 @@ export default async function handleResourceExport(
if (disposition) {
try {
const parsed = parseContentDisposition(disposition);
const parsed = contentDisposition.parse(disposition);
if (parsed?.parameters?.filename) {
fileName = parsed.parameters.filename;
}
+2 -11
View File
@@ -22,13 +22,12 @@ import {
createListenerMiddleware,
StoreEnhancer,
} from '@reduxjs/toolkit';
import type { AnyAction } from 'redux';
import {
useDispatch,
useSelector,
type TypedUseSelectorHook,
} from 'react-redux';
import thunk, { type ThunkDispatch } from 'redux-thunk';
import thunk from 'redux-thunk';
import { api } from 'src/hooks/apiResources/queryApi';
import messageToastReducer from 'src/components/MessageToasts/reducers';
import charts from 'src/components/Chart/chartReducer';
@@ -189,14 +188,6 @@ export type RootState = ReturnType<typeof store.getState>;
// thunks resolve correctly), and `useAppSelector` infers `RootState` without
// callers having to annotate every selector. Required ahead of the
// react-redux v8+ bump, which tightens dispatch typing — see #39927.
//
// AppDispatch is declared as ThunkDispatch & store.dispatch rather than
// `typeof store.dispatch` because Superset annotates getMiddleware as
// ConfigureStoreOptions['middleware'], which erases the middleware tuple type
// and leaves store.dispatch typed as Dispatch<AnyAction>. The intersection
// restores thunk support without requiring a wider refactor of the middleware
// setup.
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction> &
typeof store.dispatch;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
+146 -146
View File
@@ -23,21 +23,21 @@
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.1",
"@types/node": "^25.8.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"eslint": "^10.4.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-lodash": "^8.0.0",
"globals": "^17.6.0",
"jest": "^29.7.0",
"prettier": "^3.8.3",
"ts-jest": "^29.4.10",
"ts-jest": "^29.4.9",
"ts-node": "^10.9.2",
"tscw-config": "^1.1.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.59.4"
"typescript-eslint": "^8.59.3"
},
"engines": {
"node": "^22.22.0",
@@ -1798,9 +1798,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1844,17 +1844,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz",
"integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/type-utils": "8.59.4",
"@typescript-eslint/utils": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/type-utils": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1867,7 +1867,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.59.4",
"@typescript-eslint/parser": "^8.59.3",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -1883,16 +1883,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz",
"integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3"
},
"engines": {
@@ -1908,14 +1908,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz",
"integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.59.4",
"@typescript-eslint/types": "^8.59.4",
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"debug": "^4.4.3"
},
"engines": {
@@ -1930,14 +1930,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4"
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1948,9 +1948,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz",
"integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1965,15 +1965,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz",
"integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/utils": "8.59.4",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1990,9 +1990,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2004,16 +2004,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz",
"integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.59.4",
"@typescript-eslint/tsconfig-utils": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/project-service": "8.59.3",
"@typescript-eslint/tsconfig-utils": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -2071,16 +2071,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz",
"integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4"
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2095,13 +2095,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/types": "8.59.3",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -5695,9 +5695,9 @@
}
},
"node_modules/semver": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -6016,9 +6016,9 @@
}
},
"node_modules/ts-jest": {
"version": "29.4.10",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.10.tgz",
"integrity": "sha512-vMTlTTtvz5aKZgzOoc7DQ5TzAL2fCzl8JnG1+ZpwjQa/g0xLlwE44yQ+1Cao9ZP1xVv9y5g34IFXEiqGOGFBUA==",
"version": "29.4.9",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz",
"integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6028,7 +6028,7 @@
"json5": "^2.2.3",
"lodash.memoize": "^4.1.2",
"make-error": "^1.3.6",
"semver": "^7.8.0",
"semver": "^7.7.4",
"type-fest": "^4.41.0",
"yargs-parser": "^21.1.1"
},
@@ -6199,16 +6199,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz",
"integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
"integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.59.4",
"@typescript-eslint/parser": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/utils": "8.59.4"
"@typescript-eslint/eslint-plugin": "8.59.3",
"@typescript-eslint/parser": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7894,9 +7894,9 @@
"dev": true
},
"@types/node": {
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"dev": true,
"requires": {
"undici-types": ">=7.24.0 <7.24.7"
@@ -7938,16 +7938,16 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz",
"integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/type-utils": "8.59.4",
"@typescript-eslint/utils": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/type-utils": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -7962,75 +7962,75 @@
}
},
"@typescript-eslint/parser": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz",
"integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3"
}
},
"@typescript-eslint/project-service": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz",
"integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
"dev": true,
"requires": {
"@typescript-eslint/tsconfig-utils": "^8.59.4",
"@typescript-eslint/types": "^8.59.4",
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"debug": "^4.4.3"
}
},
"@typescript-eslint/scope-manager": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4"
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
}
},
"@typescript-eslint/tsconfig-utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz",
"integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"dev": true,
"requires": {}
},
"@typescript-eslint/type-utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz",
"integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/utils": "8.59.4",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
}
},
"@typescript-eslint/types": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz",
"integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"dev": true,
"requires": {
"@typescript-eslint/project-service": "8.59.4",
"@typescript-eslint/tsconfig-utils": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4",
"@typescript-eslint/project-service": "8.59.3",
"@typescript-eslint/tsconfig-utils": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -8065,24 +8065,24 @@
}
},
"@typescript-eslint/utils": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz",
"integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4"
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3"
}
},
"@typescript-eslint/visitor-keys": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/types": "8.59.3",
"eslint-visitor-keys": "^5.0.0"
},
"dependencies": {
@@ -10730,9 +10730,9 @@
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
},
"semver": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="
},
"shebang-command": {
"version": "2.0.0",
@@ -10954,9 +10954,9 @@
"requires": {}
},
"ts-jest": {
"version": "29.4.10",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.10.tgz",
"integrity": "sha512-vMTlTTtvz5aKZgzOoc7DQ5TzAL2fCzl8JnG1+ZpwjQa/g0xLlwE44yQ+1Cao9ZP1xVv9y5g34IFXEiqGOGFBUA==",
"version": "29.4.9",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz",
"integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==",
"dev": true,
"requires": {
"bs-logger": "^0.2.6",
@@ -10965,7 +10965,7 @@
"json5": "^2.2.3",
"lodash.memoize": "^4.1.2",
"make-error": "^1.3.6",
"semver": "^7.8.0",
"semver": "^7.7.4",
"type-fest": "^4.41.0",
"yargs-parser": "^21.1.1"
}
@@ -11044,15 +11044,15 @@
"dev": true
},
"typescript-eslint": {
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz",
"integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
"integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
"dev": true,
"requires": {
"@typescript-eslint/eslint-plugin": "8.59.4",
"@typescript-eslint/parser": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/utils": "8.59.4"
"@typescript-eslint/eslint-plugin": "8.59.3",
"@typescript-eslint/parser": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3"
}
},
"uglify-js": {
+5 -5
View File
@@ -31,21 +31,21 @@
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.1",
"@types/node": "^25.8.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"eslint": "^10.4.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-lodash": "^8.0.0",
"globals": "^17.6.0",
"jest": "^29.7.0",
"prettier": "^3.8.3",
"ts-jest": "^29.4.10",
"ts-jest": "^29.4.9",
"ts-node": "^10.9.2",
"tscw-config": "^1.1.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.59.4"
"typescript-eslint": "^8.59.3"
},
"engines": {
"node": "^22.22.0",
@@ -62,21 +62,6 @@ def build_uuid_to_id_map(position: dict[str, Any]) -> dict[str, int]:
}
def _remap_charts_in_scope(container: dict[str, Any], id_map: dict[int, int]) -> None:
"""Remap source-env chart IDs in ``container["chartsInScope"]`` in place.
``chartsInScope`` is a denormalized cache of the charts a filter (native
or cross-filter) currently applies to. Both surfaces share this contract,
so they share this remap. Unresolvable IDs are dropped rather than
passed through, matching the convention used for ``scope.excluded``.
"""
charts_in_scope = container.get("chartsInScope")
if isinstance(charts_in_scope, list):
container["chartsInScope"] = [
id_map[old_id] for old_id in charts_in_scope if old_id in id_map
]
def update_id_refs( # pylint: disable=too-many-locals # noqa: C901
config: dict[str, Any],
chart_ids: dict[str, int],
@@ -160,8 +145,6 @@ def update_id_refs( # pylint: disable=too-many-locals # noqa: C901
id_map[old_id] for old_id in scope_excluded if old_id in id_map
]
_remap_charts_in_scope(native_filter, id_map)
# fix display control dataset references
for customization in (
fixed.get("metadata", {}).get("chart_customization_config") or []
@@ -187,7 +170,7 @@ def update_id_refs( # pylint: disable=too-many-locals # noqa: C901
return fixed
def update_cross_filter_scoping( # noqa: C901
def update_cross_filter_scoping(
config: dict[str, Any], id_map: dict[int, int]
) -> dict[str, Any]:
# fix cross filter references
@@ -202,9 +185,6 @@ def update_cross_filter_scoping( # noqa: C901
id_map[old_id] for old_id in scope_excluded if old_id in id_map
]
# Global cross-filter chartsInScope mirrors the native-filter case.
_remap_charts_in_scope(cross_filter_global_config, id_map)
if "chart_configuration" in (metadata := fixed.get("metadata", {})):
# Build remapped configuration in a single pass for clarity/readability.
new_chart_configuration: dict[str, Any] = {}
@@ -232,11 +212,6 @@ def update_cross_filter_scoping( # noqa: C901
if old_id in id_map
]
# Cross-filter chartsInScope mirrors the native-filter case.
cross_filters = chart_config.get("crossFilters")
if isinstance(cross_filters, dict):
_remap_charts_in_scope(cross_filters, id_map)
new_chart_configuration[str(new_id)] = chart_config
metadata["chart_configuration"] = new_chart_configuration
+1 -4
View File
@@ -34,10 +34,7 @@ from superset.commands.report.exceptions import (
)
from superset.daos.chart import ChartDAO
from superset.daos.dashboard import DashboardDAO
from superset.reports.models import (
ReportCreationMethod,
ReportScheduleType,
)
from superset.reports.models import ReportCreationMethod, ReportScheduleType
from superset.reports.types import ReportScheduleExtra
from superset.utils import json
+1 -29
View File
@@ -33,20 +33,12 @@ from superset.commands.report.exceptions import (
ReportScheduleNameUniquenessValidationError,
ReportScheduleNotFoundError,
ReportScheduleUpdateFailedError,
ReportScheduleUserEmailNotFoundError,
)
from superset.daos.database import DatabaseDAO
from superset.daos.report import ReportScheduleDAO
from superset.exceptions import SupersetSecurityException
from superset.reports.models import (
ReportCreationMethod,
ReportRecipientType,
ReportSchedule,
ReportScheduleType,
ReportState,
)
from superset.reports.models import ReportSchedule, ReportScheduleType, ReportState
from superset.utils import json
from superset.utils.core import get_user_email
from superset.utils.decorators import on_error, transaction
logger = logging.getLogger(__name__)
@@ -97,26 +89,6 @@ class UpdateReportScheduleCommand(UpdateMixin, BaseReportScheduleCommand):
):
self._properties["last_state"] = ReportState.NOOP
# For reports created from charts or dashboards the recipient must always
# be the requesting user's own email address.
if (
self._model.creation_method
in (
ReportCreationMethod.CHARTS,
ReportCreationMethod.DASHBOARDS,
)
and "recipients" in self._properties
):
if user_email := get_user_email():
self._properties["recipients"] = [
{
"type": ReportRecipientType.EMAIL,
"recipient_config_json": {"target": user_email},
}
]
else:
exceptions.append(ReportScheduleUserEmailNotFoundError())
# Validate name/type uniqueness if either is changing
if name != self._model.name or report_type != self._model.type:
if not ReportScheduleDAO.validate_update_uniqueness(
+1 -4
View File
@@ -2011,10 +2011,7 @@ SLACK_CACHE_TIMEOUT = int(timedelta(days=1).total_seconds())
# For workspaces with 10k+ channels, consider increasing to 10
SLACK_API_RATE_LIMIT_RETRY_COUNT = 2
# The webdriver to use for generating reports when using Selenium (not Playwright).
# This setting is ignored when PLAYWRIGHT_REPORTS_AND_THUMBNAILS is enabled, as
# Playwright always uses Chromium regardless of this value.
# Use one of the following:
# The webdriver to use for generating reports. Use one of the following
# firefox
# Requires: geckodriver and firefox installations
# Limitations: can be buggy at times
+2 -40
View File
@@ -21,11 +21,10 @@ import logging
import re
from datetime import datetime
from re import Pattern
from typing import Any, Callable, Optional, TYPE_CHECKING
from typing import Any, Optional, TYPE_CHECKING
from flask_babel import gettext as __
from sqlalchemy import types
from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, INTERVAL, JSON
from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, JSON
from sqlalchemy.dialects.postgresql.base import PGInspector
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.engine.url import URL
@@ -136,34 +135,6 @@ def parse_options(connect_args: dict[str, Any]) -> dict[str, str]:
return {token[0]: token[1] for token in tokens}
def _normalize_interval(v: Any) -> Optional[float]:
"""Convert PostgreSQL INTERVAL values to milliseconds.
psycopg2 and psycopg3 always return INTERVAL values as datetime.timedelta
objects. We convert to milliseconds so users can apply the built-in
"DURATION" number format for human-readable display (e.g.,
"1d 2h 30m 45s") and so the values participate cleanly in numeric
aggregations in bar/pie charts.
Returns None for the NULL case (preserves NULL semantics) and for any
unexpected non-timedelta type (avoids producing a mixed-type column
when an unfamiliar driver surfaces something other than timedelta).
"""
if v is None:
return None
if hasattr(v, "total_seconds"):
return v.total_seconds() * 1000
# Defensive: psycopg2/3 should always hand us a timedelta. If a future
# driver doesn't, surface the surprise in the logs rather than silently
# dropping the value so operators can diagnose it.
logger.warning(
"Cannot normalize PostgreSQL INTERVAL value of type %s to numeric; "
"returning None.",
type(v).__name__,
)
return None
class PostgresBaseEngineSpec(BaseEngineSpec):
"""Abstract class for Postgres 'like' databases"""
@@ -555,17 +526,8 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec):
ENUM(),
GenericDataType.STRING,
),
(
re.compile(r"^interval", re.IGNORECASE),
INTERVAL(),
GenericDataType.NUMERIC,
),
)
column_type_mutators: dict[types.TypeEngine, Callable[[Any], Any]] = {
INTERVAL: _normalize_interval,
}
@classmethod
def get_schema_from_engine_params(
cls,
+348
View File
@@ -0,0 +1,348 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
An interface to any server implementing the Semantic Layer REST API.
"""
from __future__ import annotations
from typing import Any, TYPE_CHECKING, TypedDict
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from flask import g
from marshmallow import fields, Schema
from shillelagh.exceptions import UnauthenticatedError
from sqlalchemy.engine.url import URL
from superset.databases.types import EncryptedDict
from superset.databases.utils import make_url_safe
from superset.db_engine_specs.base import MetricType
from superset.db_engine_specs.shillelagh import ShillelaghEngineSpec
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import OAuth2TokenRefreshError
from superset.utils import json
if TYPE_CHECKING:
from sqlalchemy.engine.reflection import Inspector
from superset.models.core import Database
from superset.sql.parse import Table
from superset.superset_typing import ResultSetColumnType
SELECT_STAR_MESSAGE = (
"The Semantic Layer API does not support data preview, since the view is a "
"virtual table that is not materialized. An administrator should configure "
'the database in Apache Superset so that the "Disable SQL Lab data preview '
'queries" option under "Advanced""SQL Lab" is enabled.'
)
_TRUTHY = {"1", "true", "yes", "on"}
ma_plugin = MarshmallowPlugin()
class SemanticAPIParametersSchema(Schema):
"""Form schema for the Semantic Layer API connection wizard."""
host = fields.Str(
required=True,
metadata={"description": "Hostname of the Semantic Layer API server."},
)
port = fields.Int(
required=False,
allow_none=True,
metadata={"description": "Optional port (omit to use the URL default)."},
)
secure = fields.Bool(
load_default=False,
metadata={"description": "Use HTTPS to reach the server."},
)
additional_configuration = fields.Dict(
required=False,
allow_none=True,
metadata={
"description": (
"Per-tenant JSON object forwarded to the server on every call. "
"Sent as ``runtime_configuration`` when listing views and as "
"``additional_configuration`` for each view."
),
},
)
oauth2_client_info = EncryptedDict(
required=False,
allow_none=True,
metadata={
"description": (
"OAuth2 client credentials. Provide ``id`` and ``secret``; the "
"authorisation and token URIs are auto-filled from the server "
"host unless explicitly set."
),
"default": {"id": "", "secret": "", "scope": ""},
},
)
class SemanticAPIParametersType(TypedDict, total=False):
host: str
port: int | None
secure: bool
additional_configuration: dict[str, Any] | None
oauth2_client_info: dict[str, Any] | None
class SemanticAPIPropertiesType(TypedDict, total=False):
parameters: SemanticAPIParametersType
masked_encrypted_extra: str
class SemanticAPIEngineSpec(ShillelaghEngineSpec):
"""
Engine for the Semantic Layer REST API.
Connection URL::
semanticapi://<host>[:port]/[?secure=true]
OAuth2 is supported. The database's ``encrypted_extra`` should look like::
{
"oauth2_client_info": {
"id": "demo-client",
"secret": "demo-secret",
"scope": "",
"authorization_request_uri": "http://<host>:<port>/authorize",
"token_request_uri": "http://<host>:<port>/token"
}
}
Per-tenant ``additional_configuration`` is forwarded through
``extra.engine_params.connect_args.additional_configuration``.
"""
engine = "semanticapi"
engine_name = "Semantic Layer API"
default_driver = "apsw"
sqlalchemy_uri_placeholder = "semanticapi://<host>[:port]/?secure=<true|false>"
parameters_schema = SemanticAPIParametersSchema()
# OAuth 2.0 — the authorisation and token URIs live on each database's
# ``encrypted_extra.oauth2_client_info`` since they're derived from that
# database's server.
supports_oauth2 = True
oauth2_exception = (UnauthenticatedError, OAuth2TokenRefreshError)
oauth2_token_request_type = "data" # noqa: S105
# pylint: disable=invalid-name
encrypted_extra_sensitive_fields = {
"$.oauth2_client_info.secret": "OAuth2 Client Secret",
}
@classmethod
def build_sqlalchemy_uri(
cls,
parameters: SemanticAPIParametersType,
encrypted_extra: dict[str, Any] | None = None,
) -> str:
"""
Turn form parameters into a ``semanticapi://`` URL.
``additional_configuration`` is JSON-encoded onto the query string,
and the OAuth2 ``authorization_request_uri`` / ``token_request_uri``
are filled in (if missing) from the server's host:port.
"""
host = parameters.get("host") or ""
port = parameters.get("port")
secure = bool(parameters.get("secure"))
query: dict[str, str] = {}
if secure:
query["secure"] = "true"
if config := parameters.get("additional_configuration"):
query["additional_configuration"] = (
config if isinstance(config, str) else json.dumps(config)
)
if encrypted_extra and (oauth2 := encrypted_extra.get("oauth2_client_info")):
scheme = "https" if secure else "http"
netloc = f"{host}:{port}" if port else host
base = f"{scheme}://{netloc}"
oauth2.setdefault("authorization_request_uri", f"{base}/authorize")
oauth2.setdefault("token_request_uri", f"{base}/token")
oauth2.setdefault("scope", "")
return str(URL.create(cls.engine, host=host, port=port, query=query))
@classmethod
def get_parameters_from_uri(
cls,
uri: str,
encrypted_extra: dict[str, Any] | None = None,
) -> SemanticAPIParametersType:
"""
Inverse of :meth:`build_sqlalchemy_uri` for repopulating the form.
"""
url = make_url_safe(uri)
parameters: SemanticAPIParametersType = {
"host": url.host or "",
"port": url.port,
"secure": str(url.query.get("secure", "")).lower() in _TRUTHY,
}
if raw := url.query.get("additional_configuration"):
try:
parameters["additional_configuration"] = json.loads(raw)
except json.JSONDecodeError:
parameters["additional_configuration"] = None
if encrypted_extra and "oauth2_client_info" in encrypted_extra:
parameters["oauth2_client_info"] = encrypted_extra["oauth2_client_info"]
return parameters
@classmethod
def parameters_json_schema(cls) -> Any:
"""Return the form's OpenAPI schema, used by the frontend to render it."""
# imported lazily because ``superset.databases.schemas`` touches the
# Flask app context at import time.
from superset.databases.schemas import encrypted_field_properties
if not cls.parameters_schema:
return None
spec = APISpec(
title="Database Parameters",
version="1.0.0",
openapi_version="3.0.0",
plugins=[ma_plugin],
)
ma_plugin.init_spec(spec)
ma_plugin.converter.add_attribute_function(encrypted_field_properties)
spec.components.schema(cls.__name__, schema=cls.parameters_schema)
return spec.to_dict()["components"]["schemas"][cls.__name__]
@classmethod
def validate_parameters(
cls,
properties: SemanticAPIPropertiesType,
) -> list[SupersetError]:
"""Surface missing ``host`` (the only field we strictly require)."""
errors: list[SupersetError] = []
parameters = properties.get("parameters", {})
if not parameters.get("host"):
errors.append(
SupersetError(
message="Host is required.",
error_type=SupersetErrorType.CONNECTION_MISSING_PARAMETERS_ERROR,
level=ErrorLevel.WARNING,
extra={"missing": ["host"]},
),
)
return errors
@classmethod
def needs_oauth2(cls, ex: Exception) -> bool:
"""
``UnauthenticatedError`` from the adapter signals an expired/missing
bearer token i.e. we need to run the OAuth2 dance.
"""
return bool(g) and hasattr(g, "user") and isinstance(ex, cls.oauth2_exception)
@classmethod
def impersonate_user(
cls,
database: Database, # noqa: ARG003
username: str | None, # noqa: ARG003
user_token: str | None,
url: URL,
engine_kwargs: dict[str, Any],
) -> tuple[URL, dict[str, Any]]:
"""
Inject the user's OAuth2 access token into the URL query string so
that the dialect (and therefore the adapter) sends it as a Bearer.
"""
if user_token:
url = url.update_query_dict({"access_token": user_token})
return url, engine_kwargs
@classmethod
def adjust_engine_params(
cls,
uri: URL,
connect_args: dict[str, Any],
catalog: str | None = None,
schema: str | None = None,
) -> tuple[URL, dict[str, Any]]:
"""
Fold ``additional_configuration`` from the database's ``extra`` field
(placed by the user under ``engine_params.connect_args``) into the URL
query string, so the dialect can pick it up.
"""
uri, connect_args = super().adjust_engine_params(
uri, connect_args, catalog, schema
)
if (config := connect_args.pop("additional_configuration", None)) is not None:
query = dict(uri.query)
query["additional_configuration"] = (
config if isinstance(config, str) else json.dumps(config)
)
uri = uri.set(query=query)
return uri, connect_args
@classmethod
def select_star(cls, *args: Any, **kwargs: Any) -> str:
"""
Return a stand-in ``SELECT *`` that explains why preview is disabled.
"""
message = SELECT_STAR_MESSAGE.replace("'", "''")
return f"SELECT '{message}' AS warning"
@classmethod
def get_columns(
cls,
inspector: Inspector,
table: Table,
options: dict[str, Any] | None = None,
) -> list[ResultSetColumnType]:
"""
Return only the view's dimensions; metrics are surfaced by ``get_metrics``.
"""
columns: list[ResultSetColumnType] = []
for column in inspector.get_columns(table.table, table.schema):
if "computed" in column:
continue
column["column_name"] = column["name"]
columns.append(column)
return columns
@classmethod
def get_metrics(
cls,
database: Database, # noqa: ARG003
inspector: Inspector,
table: Table,
) -> list[MetricType]:
"""
Translate the view's metric columns into Superset metric definitions.
"""
return [
{
"metric_name": column["name"],
"expression": column["computed"]["sqltext"],
"description": column["comment"],
}
for column in inspector.get_columns(table.table, table.schema)
if "computed" in column
]
@@ -1,16 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
@@ -1,312 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Pydantic schemas for annotation layer and annotation responses."""
from __future__ import annotations
from datetime import datetime
from typing import Annotated, Any, Literal
from pydantic import (
BaseModel,
ConfigDict,
Field,
field_validator,
model_validator,
PositiveInt,
)
from superset.daos.base import ColumnOperator, ColumnOperatorEnum
from superset.mcp_service.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
from superset.mcp_service.system.schemas import PaginationInfo
from superset.mcp_service.utils.schema_utils import (
parse_json_or_list,
parse_json_or_model_list,
)
DEFAULT_LAYER_COLUMNS = ["id", "name", "descr"]
DEFAULT_ANNOTATION_COLUMNS = ["id", "short_descr", "start_dttm", "end_dttm"]
class AnnotationLayerFilter(ColumnOperator):
"""Filter object for annotation layer listing."""
col: Literal["name"] = Field(
...,
description="Column to filter on. Supported: 'name'.",
)
opr: ColumnOperatorEnum = Field(..., description="Filter operator.")
value: str | int | float | bool | list[str | int | float | bool] = Field(
..., description="Value to filter by."
)
class AnnotationFilter(ColumnOperator):
"""Filter object for annotation listing."""
col: Literal["short_descr"] = Field(
...,
description="Column to filter on. Supported: 'short_descr'.",
)
opr: ColumnOperatorEnum = Field(..., description="Filter operator.")
value: str | int | float | bool | list[str | int | float | bool] = Field(
..., description="Value to filter by."
)
class AnnotationLayerInfo(BaseModel):
id: int | None = Field(None, description="Annotation layer ID")
name: str | None = Field(None, description="Annotation layer name")
descr: str | None = Field(None, description="Annotation layer description")
changed_on: str | datetime | None = Field(
None, description="Last modification timestamp"
)
created_on: str | datetime | None = Field(None, description="Creation timestamp")
model_config = ConfigDict(from_attributes=True, ser_json_timedelta="iso8601")
class AnnotationLayerList(BaseModel):
annotation_layers: list[AnnotationLayerInfo]
count: int
total_count: int
page: int
page_size: int
total_pages: int
has_previous: bool
has_next: bool
columns_requested: list[str] = Field(default_factory=list)
columns_loaded: list[str] = Field(default_factory=list)
columns_available: list[str] = Field(default_factory=list)
sortable_columns: list[str] = Field(default_factory=list)
filters_applied: list[AnnotationLayerFilter] = Field(default_factory=list)
pagination: PaginationInfo | None = None
timestamp: datetime | None = None
model_config = ConfigDict(ser_json_timedelta="iso8601")
class ListAnnotationLayersRequest(BaseModel):
"""Request schema for list_annotation_layers."""
filters: Annotated[
list[AnnotationLayerFilter],
Field(
default_factory=list,
description="List of filter objects. Cannot be combined with 'search'.",
),
]
select_columns: Annotated[
list[str],
Field(
default_factory=list,
description="Columns to include in the response.",
),
]
search: Annotated[
str | None,
Field(
default=None,
description="Text search across annotation layer name and description.",
),
]
order_column: Annotated[
str | None, Field(default=None, description="Column to order results by.")
]
order_direction: Annotated[
Literal["asc", "desc"],
Field(default="desc", description="Sort direction."),
]
page: Annotated[
PositiveInt,
Field(default=1, description="Page number (1-based)."),
]
page_size: Annotated[
int,
Field(
default=DEFAULT_PAGE_SIZE,
gt=0,
le=MAX_PAGE_SIZE,
description=f"Items per page (max {MAX_PAGE_SIZE}).",
),
]
@field_validator("filters", mode="before")
@classmethod
def parse_filters(cls, v: Any) -> list[AnnotationLayerFilter]:
return parse_json_or_model_list(v, AnnotationLayerFilter, "filters")
@field_validator("select_columns", mode="before")
@classmethod
def parse_columns(cls, v: Any) -> list[str]:
return parse_json_or_list(v, "select_columns")
@model_validator(mode="after")
def validate_search_and_filters(self) -> "ListAnnotationLayersRequest":
if self.search and self.filters:
raise ValueError("Cannot use both 'search' and 'filters' simultaneously.")
return self
class GetAnnotationLayerInfoRequest(BaseModel):
"""Request schema for get_annotation_layer_info."""
id: Annotated[int, Field(description="Annotation layer ID.")]
class AnnotationInfo(BaseModel):
id: int | None = Field(None, description="Annotation ID")
short_descr: str | None = Field(None, description="Short description")
long_descr: str | None = Field(None, description="Long description")
start_dttm: str | datetime | None = Field(None, description="Start datetime")
end_dttm: str | datetime | None = Field(None, description="End datetime")
json_metadata: str | None = Field(None, description="JSON metadata")
layer_id: int | None = Field(None, description="Parent annotation layer ID")
model_config = ConfigDict(from_attributes=True, ser_json_timedelta="iso8601")
class AnnotationList(BaseModel):
annotations: list[AnnotationInfo]
count: int
total_count: int
page: int
page_size: int
total_pages: int
has_previous: bool
has_next: bool
# layer_id defaults to 0; the tool sets it after ModelListCore constructs this
# object. ModelListCore does not know about this domain-specific field.
layer_id: int = 0
columns_requested: list[str] = Field(default_factory=list)
columns_loaded: list[str] = Field(default_factory=list)
columns_available: list[str] = Field(default_factory=list)
sortable_columns: list[str] = Field(default_factory=list)
filters_applied: list[ColumnOperator] = Field(default_factory=list)
pagination: PaginationInfo | None = None
timestamp: datetime | None = None
model_config = ConfigDict(ser_json_timedelta="iso8601")
class ListLayerAnnotationsRequest(BaseModel):
"""Request schema for list_layer_annotations."""
layer_id: Annotated[
int, Field(description="Annotation layer ID to list annotations for.")
]
filters: Annotated[
list[AnnotationFilter],
Field(
default_factory=list,
description="List of filter objects. Cannot be combined with 'search'.",
),
]
select_columns: Annotated[
list[str],
Field(default_factory=list, description="Columns to include in the response."),
]
search: Annotated[
str | None,
Field(
default=None,
description="Text search across annotation short and long description.",
),
]
order_column: Annotated[
str | None, Field(default=None, description="Column to order results by.")
]
order_direction: Annotated[
Literal["asc", "desc"],
Field(default="desc", description="Sort direction."),
]
page: Annotated[
PositiveInt,
Field(default=1, description="Page number (1-based)."),
]
page_size: Annotated[
int,
Field(
default=DEFAULT_PAGE_SIZE,
gt=0,
le=MAX_PAGE_SIZE,
description=f"Items per page (max {MAX_PAGE_SIZE}).",
),
]
@field_validator("filters", mode="before")
@classmethod
def parse_filters(cls, v: Any) -> list[AnnotationFilter]:
return parse_json_or_model_list(v, AnnotationFilter, "filters")
@field_validator("select_columns", mode="before")
@classmethod
def parse_columns(cls, v: Any) -> list[str]:
return parse_json_or_list(v, "select_columns")
@model_validator(mode="after")
def validate_search_and_filters(self) -> "ListLayerAnnotationsRequest":
if self.search and self.filters:
raise ValueError("Cannot use both 'search' and 'filters' simultaneously.")
return self
class GetLayerAnnotationInfoRequest(BaseModel):
"""Request schema for get_layer_annotation_info."""
layer_id: Annotated[int, Field(description="Annotation layer ID.")]
annotation_id: Annotated[int, Field(description="Annotation ID.")]
class AnnotationLayerError(BaseModel):
error: str = Field(..., description="Error message")
error_type: str = Field(..., description="Type of error")
timestamp: str | datetime | None = Field(None, description="Error timestamp")
model_config = ConfigDict(ser_json_timedelta="iso8601")
@classmethod
def create(cls, error: str, error_type: str) -> "AnnotationLayerError":
from datetime import timezone
return cls(
error=error,
error_type=error_type,
timestamp=datetime.now(timezone.utc),
)
def serialize_annotation_layer(obj: Any) -> AnnotationLayerInfo | None:
if not obj:
return None
return AnnotationLayerInfo(
id=getattr(obj, "id", None),
name=getattr(obj, "name", None),
descr=getattr(obj, "descr", None),
changed_on=getattr(obj, "changed_on", None),
created_on=getattr(obj, "created_on", None),
)
def serialize_annotation(obj: Any) -> AnnotationInfo | None:
if not obj:
return None
return AnnotationInfo(
id=getattr(obj, "id", None),
short_descr=getattr(obj, "short_descr", None),
long_descr=getattr(obj, "long_descr", None),
start_dttm=getattr(obj, "start_dttm", None),
end_dttm=getattr(obj, "end_dttm", None),
json_metadata=getattr(obj, "json_metadata", None),
layer_id=getattr(obj, "layer_id", None),
)
@@ -1,28 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from .get_annotation_layer_info import get_annotation_layer_info
from .get_layer_annotation_info import get_layer_annotation_info
from .list_annotation_layers import list_annotation_layers
from .list_layer_annotations import list_layer_annotations
__all__ = [
"list_annotation_layers",
"get_annotation_layer_info",
"list_layer_annotations",
"get_layer_annotation_info",
]
@@ -1,97 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Get annotation layer info FastMCP tool."""
import logging
from datetime import datetime, timezone
from fastmcp import Context
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.extensions import event_logger
from superset.mcp_service.annotation_layer.schemas import (
AnnotationLayerError,
AnnotationLayerInfo,
GetAnnotationLayerInfoRequest,
serialize_annotation_layer,
)
from superset.mcp_service.mcp_core import ModelGetInfoCore
logger = logging.getLogger(__name__)
@tool(
tags=["discovery"],
class_permission_name="Annotation",
annotations=ToolAnnotations(
title="Get annotation layer info",
readOnlyHint=True,
destructiveHint=False,
),
)
async def get_annotation_layer_info(
request: GetAnnotationLayerInfoRequest,
ctx: Context,
) -> AnnotationLayerInfo | AnnotationLayerError:
"""Get detailed information about an annotation layer by ID.
Returns the layer's name, description, and timestamps.
Example:
```json
{"id": 1}
```
"""
await ctx.info("Retrieving annotation layer: id=%s" % (request.id,))
try:
from superset.daos.annotation_layer import AnnotationLayerDAO
with event_logger.log_context(action="mcp.get_annotation_layer_info.lookup"):
get_tool = ModelGetInfoCore(
dao_class=AnnotationLayerDAO,
output_schema=AnnotationLayerInfo,
error_schema=AnnotationLayerError,
serializer=serialize_annotation_layer,
supports_slug=False,
logger=logger,
)
result = get_tool.run_tool(request.id)
if isinstance(result, AnnotationLayerInfo):
await ctx.info(
"Annotation layer retrieved: id=%s, name=%s" % (result.id, result.name)
)
else:
await ctx.warning(
"Annotation layer not found: id=%s, error_type=%s"
% (request.id, result.error_type)
)
return result
except Exception as e:
await ctx.error(
"Annotation layer lookup failed: id=%s, error=%s, error_type=%s"
% (request.id, str(e), type(e).__name__)
)
return AnnotationLayerError(
error=f"Failed to get annotation layer info: {str(e)}",
error_type="InternalError",
timestamp=datetime.now(timezone.utc),
)
@@ -1,130 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Get a single annotation within a layer FastMCP tool."""
import logging
from datetime import datetime, timezone
from fastmcp import Context
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.extensions import event_logger
from superset.mcp_service.annotation_layer.schemas import (
AnnotationInfo,
AnnotationLayerError,
GetLayerAnnotationInfoRequest,
serialize_annotation,
)
logger = logging.getLogger(__name__)
@tool(
tags=["discovery"],
class_permission_name="Annotation",
annotations=ToolAnnotations(
title="Get annotation info",
readOnlyHint=True,
destructiveHint=False,
),
)
async def get_layer_annotation_info(
request: GetLayerAnnotationInfoRequest,
ctx: Context,
) -> AnnotationInfo | AnnotationLayerError:
"""Get detailed information about a specific annotation within a layer.
Both layer_id and annotation_id are required. Returns an error if the
annotation does not belong to the specified layer.
Example:
```json
{"layer_id": 1, "annotation_id": 42}
```
"""
await ctx.info(
"Retrieving annotation: layer_id=%s, annotation_id=%s"
% (request.layer_id, request.annotation_id)
)
try:
from superset.daos.annotation_layer import AnnotationDAO, AnnotationLayerDAO
# Verify the layer exists
with event_logger.log_context(
action="mcp.get_layer_annotation_info.layer_lookup"
):
layer = AnnotationLayerDAO.find_by_id(request.layer_id)
if layer is None:
await ctx.warning("Annotation layer not found: id=%s" % (request.layer_id,))
return AnnotationLayerError.create(
error=f"Annotation layer with id '{request.layer_id}' not found",
error_type="not_found",
)
# Fetch the annotation
with event_logger.log_context(
action="mcp.get_layer_annotation_info.annotation_lookup"
):
annotation = AnnotationDAO.find_by_id(request.annotation_id)
if annotation is None:
await ctx.warning(
"Annotation not found: annotation_id=%s" % (request.annotation_id,)
)
return AnnotationLayerError.create(
error=f"Annotation with id '{request.annotation_id}' not found",
error_type="not_found",
)
# Verify the annotation belongs to the requested layer
if getattr(annotation, "layer_id", None) != request.layer_id:
await ctx.warning(
"Annotation %s does not belong to layer %s"
% (request.annotation_id, request.layer_id)
)
return AnnotationLayerError.create(
error=(
f"Annotation '{request.annotation_id}' does not belong to "
f"layer '{request.layer_id}'"
),
error_type="not_found",
)
result = serialize_annotation(annotation)
await ctx.info(
"Annotation retrieved: id=%s, short_descr=%s"
% (result.id if result else None, result.short_descr if result else None)
)
return result or AnnotationLayerError.create(
error="Failed to serialize annotation",
error_type="SerializationError",
)
except Exception as e:
await ctx.error(
"Annotation lookup failed: layer_id=%s, annotation_id=%s, "
"error=%s, error_type=%s"
% (request.layer_id, request.annotation_id, str(e), type(e).__name__)
)
return AnnotationLayerError(
error=f"Failed to get annotation info: {str(e)}",
error_type="InternalError",
timestamp=datetime.now(timezone.utc),
)
@@ -1,123 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""List annotation layers FastMCP tool."""
import logging
from fastmcp import Context
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.extensions import event_logger
from superset.mcp_service.annotation_layer.schemas import (
AnnotationLayerError,
AnnotationLayerFilter,
AnnotationLayerInfo,
AnnotationLayerList,
DEFAULT_LAYER_COLUMNS,
ListAnnotationLayersRequest,
serialize_annotation_layer,
)
from superset.mcp_service.mcp_core import ModelListCore
logger = logging.getLogger(__name__)
_DEFAULT_REQUEST = ListAnnotationLayersRequest()
_ALL_LAYER_COLUMNS = ["id", "name", "descr", "changed_on", "created_on"]
_SORTABLE_LAYER_COLUMNS = ["id", "name", "changed_on", "created_on"]
@tool(
tags=["core"],
class_permission_name="Annotation",
annotations=ToolAnnotations(
title="List annotation layers",
readOnlyHint=True,
destructiveHint=False,
),
)
async def list_annotation_layers(
request: ListAnnotationLayersRequest | None = None,
ctx: Context | None = None,
) -> AnnotationLayerList | AnnotationLayerError:
"""List annotation layers with filtering, search, and pagination.
Returns annotation layer metadata including name and description.
Sortable columns for order_column: id, name, changed_on, created_on
"""
if ctx is None:
raise RuntimeError("FastMCP context is required for list_annotation_layers")
request = request or _DEFAULT_REQUEST.model_copy(deep=True)
await ctx.info(
"Listing annotation layers: page=%s, page_size=%s, search=%s"
% (request.page, request.page_size, request.search)
)
try:
from superset.daos.annotation_layer import AnnotationLayerDAO
def _serialize(
obj: object, cols: list[str] | None
) -> AnnotationLayerInfo | None:
return serialize_annotation_layer(obj)
list_tool = ModelListCore(
dao_class=AnnotationLayerDAO,
output_schema=AnnotationLayerInfo,
item_serializer=_serialize,
filter_type=AnnotationLayerFilter,
default_columns=DEFAULT_LAYER_COLUMNS,
search_columns=["name", "descr"],
list_field_name="annotation_layers",
output_list_schema=AnnotationLayerList,
all_columns=_ALL_LAYER_COLUMNS,
sortable_columns=_SORTABLE_LAYER_COLUMNS,
logger=logger,
)
with event_logger.log_context(action="mcp.list_annotation_layers.query"):
result = list_tool.run_tool(
filters=request.filters,
search=request.search,
select_columns=request.select_columns,
order_column=request.order_column,
order_direction=request.order_direction,
page=max(request.page - 1, 0),
page_size=request.page_size,
)
await ctx.info(
"Annotation layers listed: count=%s, total_count=%s"
% (
len(result.annotation_layers)
if hasattr(result, "annotation_layers")
else 0,
getattr(result, "total_count", None),
)
)
return result
except Exception as e:
await ctx.error(
"Annotation layer listing failed: error=%s, error_type=%s"
% (str(e), type(e).__name__)
)
raise
@@ -1,149 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""List annotations within a layer FastMCP tool."""
import logging
from datetime import datetime, timezone
from fastmcp import Context
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.daos.base import ColumnOperator, ColumnOperatorEnum
from superset.extensions import event_logger
from superset.mcp_service.annotation_layer.schemas import (
AnnotationFilter,
AnnotationInfo,
AnnotationLayerError,
AnnotationList,
DEFAULT_ANNOTATION_COLUMNS,
ListLayerAnnotationsRequest,
serialize_annotation,
)
from superset.mcp_service.mcp_core import ModelListCore
logger = logging.getLogger(__name__)
_ALL_ANNOTATION_COLUMNS = [
"id",
"short_descr",
"long_descr",
"start_dttm",
"end_dttm",
"json_metadata",
"layer_id",
]
_SORTABLE_ANNOTATION_COLUMNS = ["id", "short_descr", "start_dttm", "end_dttm"]
@tool(
tags=["core"],
class_permission_name="Annotation",
annotations=ToolAnnotations(
title="List annotations in a layer",
readOnlyHint=True,
destructiveHint=False,
),
)
async def list_layer_annotations(
request: ListLayerAnnotationsRequest,
ctx: Context,
) -> AnnotationList | AnnotationLayerError:
"""List annotations within a specific annotation layer.
The layer_id parameter is required and scopes all results to that layer.
Sortable columns for order_column: id, short_descr, start_dttm, end_dttm
Example:
```json
{"layer_id": 1, "page": 1, "page_size": 25}
```
"""
await ctx.info(
"Listing annotations: layer_id=%s, page=%s, page_size=%s, search=%s"
% (request.layer_id, request.page, request.page_size, request.search)
)
try:
from superset.daos.annotation_layer import AnnotationDAO, AnnotationLayerDAO
# Verify the layer exists before listing
layer = AnnotationLayerDAO.find_by_id(request.layer_id)
if layer is None:
await ctx.warning("Annotation layer not found: id=%s" % (request.layer_id,))
return AnnotationLayerError.create(
error=f"Annotation layer with id '{request.layer_id}' not found",
error_type="not_found",
)
# Prepend the layer_id filter so results are scoped to this layer
layer_filter = ColumnOperator(
col="layer_id", opr=ColumnOperatorEnum.eq, value=request.layer_id
)
combined_filters: list[ColumnOperator] = [layer_filter] + list(request.filters)
def _serialize(obj: object, cols: list[str] | None) -> AnnotationInfo | None:
return serialize_annotation(obj)
list_tool = ModelListCore(
dao_class=AnnotationDAO,
output_schema=AnnotationInfo,
item_serializer=_serialize,
filter_type=AnnotationFilter,
default_columns=DEFAULT_ANNOTATION_COLUMNS,
search_columns=["short_descr", "long_descr"],
list_field_name="annotations",
output_list_schema=AnnotationList,
all_columns=_ALL_ANNOTATION_COLUMNS,
sortable_columns=_SORTABLE_ANNOTATION_COLUMNS,
logger=logger,
)
with event_logger.log_context(action="mcp.list_layer_annotations.query"):
result = list_tool.run_tool(
filters=combined_filters,
search=request.search,
select_columns=request.select_columns,
order_column=request.order_column,
order_direction=request.order_direction,
page=max(request.page - 1, 0),
page_size=request.page_size,
)
result.layer_id = request.layer_id
await ctx.info(
"Annotations listed: layer_id=%s, count=%s, total_count=%s"
% (
request.layer_id,
len(result.annotations) if hasattr(result, "annotations") else 0,
getattr(result, "total_count", None),
)
)
return result
except Exception as e:
await ctx.error(
"Annotation listing failed: layer_id=%s, error=%s, error_type=%s"
% (request.layer_id, str(e), type(e).__name__)
)
return AnnotationLayerError(
error=f"Failed to list annotations: {str(e)}",
error_type="InternalError",
timestamp=datetime.now(timezone.utc),
)
-29
View File
@@ -119,12 +119,6 @@ Dashboard Management:
- generate_dashboard: Create a dashboard from chart IDs
- add_chart_to_existing_dashboard: Add a chart to an existing dashboard
Annotation Layers:
- list_annotation_layers: List annotation layers with advanced filters (1-based pagination)
- get_annotation_layer_info: Get annotation layer details by ID
- list_layer_annotations: List annotations within a layer (requires layer_id, 1-based pagination)
- get_layer_annotation_info: Get annotation details by layer_id and annotation_id
Database Connections:
- list_databases: List database connections with advanced filters (1-based pagination)
- get_database_info: Get detailed database connection info by ID (backend, capabilities)
@@ -156,7 +150,6 @@ Schema Discovery:
System Information:
- get_instance_info: Get instance-wide statistics, metadata, and current user identity
- find_users: Resolve a person's name to user IDs for use as a filter value
- health_check: Simple health check tool (takes NO parameters, call without arguments)
- generate_bug_report: Build a PII-sanitized bug report to send to Preset support
(use when the user says the MCP is broken or asks how to report an issue)
@@ -198,16 +191,6 @@ Some tools do not use a request wrapper, so follow each tool's schema
Recommended Workflows:
To filter dashboards/charts/datasets by a person ("show me what <name> is working on"):
1. find_users(request={{"query": "<name>"}}) -> resolve to user IDs
2. Pick the matching user.id from the response
3. list_dashboards(request={{"filters": [
{{"col": "created_by_fk", "opr": "eq", "value": <id>}}
]}}) same shape for list_charts / list_datasets.
(use changed_by_fk for "last modified by", or "in" with a list of IDs for
multiple matches). Do NOT pass the person's name as the search parameter —
search matches titles, not people.
To add a chart to an existing dashboard:
1. add_chart_to_existing_dashboard(dashboard_id, chart_id) -> updates dashboard directly
- If permission_denied=True is returned: inform the user they lack edit rights,
@@ -377,11 +360,6 @@ Input format:
contact details, roles, admin status, ownership, or access-list information.
- Do NOT infer access-list answers from dashboard metadata such as published status,
role restrictions, empty owner lists, or schema fields.
- find_users is sanctioned ONLY for resolving a name the user supplied into a
user ID for filtering (e.g., "what is <name> working on" -> filter
list_dashboards by created_by_fk). Do NOT use find_users to answer "who owns
X", "who can access X", "is <name> an admin", or to enumerate the directory.
Never return find_users output to the user verbatim.
- Do NOT use execute_sql to query user, role, owner, or access-list tables for this
information.
- You may reference the current user's own identity details when appropriate, such
@@ -608,12 +586,6 @@ warnings.filterwarnings(
# NOTE: Always add new prompt/resource imports here when creating new prompts/resources.
# Prompts use @mcp.prompt decorators and resources use @mcp.resource decorators.
# They register automatically on import, similar to tools.
from superset.mcp_service.annotation_layer.tool import ( # noqa: F401, E402
get_annotation_layer_info,
get_layer_annotation_info,
list_annotation_layers,
list_layer_annotations,
)
from superset.mcp_service.chart import ( # noqa: F401, E402
prompts as chart_prompts,
resources as chart_resources,
@@ -658,7 +630,6 @@ from superset.mcp_service.system import ( # noqa: F401, E402
resources as system_resources,
)
from superset.mcp_service.system.tool import ( # noqa: F401, E402
find_users,
generate_bug_report,
get_instance_info,
get_schema,
+23 -42
View File
@@ -251,7 +251,7 @@ def _generate_ascii_line_chart(data: list[Any], width: int, height: int) -> str:
lines.append("" * min(width, 60))
# Extract values and labels for plotting
values, labels, is_temporal = _extract_time_series_data(data)
values, labels = _extract_time_series_data(data)
if not values:
return "No numeric data found for line chart"
@@ -265,7 +265,7 @@ def _generate_ascii_line_chart(data: list[Any], width: int, height: int) -> str:
lines.extend(sparkline_data)
# Add trend analysis
trend_analysis = _analyze_trend(values, is_temporal=is_temporal)
trend_analysis = _analyze_trend(values)
lines.append("")
lines.append("📊 Trend Analysis:")
lines.extend(trend_analysis)
@@ -273,17 +273,10 @@ def _generate_ascii_line_chart(data: list[Any], width: int, height: int) -> str:
return "\n".join(lines)
def _extract_time_series_data(
data: list[Any],
) -> tuple[list[float], list[str], bool]:
"""Extract time series data with labels.
Returns a tuple of (values, labels, is_temporal) where is_temporal indicates
whether the label column appears to represent temporal data based on its name.
"""
def _extract_time_series_data(data: list[Any]) -> tuple[list[float], list[str]]:
"""Extract time series data with labels."""
values = []
labels = []
is_temporal = False
for row in data[:20]: # Limit points for readability
if isinstance(row, dict):
@@ -306,7 +299,6 @@ def _extract_time_series_data(
for date_word in ["date", "time", "month", "day", "year"]
):
label_val = str(val)[:10] # Truncate long dates
is_temporal = True
else:
label_val = str(val)[:8] # Truncate long strings
@@ -314,7 +306,7 @@ def _extract_time_series_data(
values.append(numeric_val)
labels.append(label_val or f"P{len(values)}")
return values, labels, is_temporal
return values, labels
def _create_enhanced_line_chart(
@@ -415,15 +407,8 @@ def _draw_line_segment(
grid[y][x] = ""
def _analyze_trend(values: list[float], *, is_temporal: bool = True) -> list[str]:
"""Analyze trend in the data.
Args:
values: Numeric data points to analyze.
is_temporal: Whether the data represents a time series. When False,
directional trend analysis is skipped because the ordering of
categorical data is arbitrary.
"""
def _analyze_trend(values: list[float]) -> list[str]:
"""Analyze trend in the data."""
if len(values) < 2:
return ["• Insufficient data for trend analysis"]
@@ -436,28 +421,24 @@ def _analyze_trend(values: list[float], *, is_temporal: bool = True) -> list[str
max_val = max(values)
avg_val = sum(values) / len(values)
if is_temporal:
# Overall trend (only meaningful for temporally ordered data)
if last_val > first_val * 1.1:
trend_icon = "📈"
trend_desc = "Strong upward trend"
elif last_val > first_val * 1.05:
trend_icon = "📊"
trend_desc = "Moderate upward trend"
elif last_val < first_val * 0.9:
trend_icon = "📉"
trend_desc = "Strong downward trend"
elif last_val < first_val * 0.95:
trend_icon = "📊"
trend_desc = "Moderate downward trend"
else:
trend_icon = "➡️"
trend_desc = "Relatively stable"
analysis.append(f"{trend_icon} {trend_desc}")
# Overall trend
if last_val > first_val * 1.1:
trend_icon = "📈"
trend_desc = "Strong upward trend"
elif last_val > first_val * 1.05:
trend_icon = "📊"
trend_desc = "Moderate upward trend"
elif last_val < first_val * 0.9:
trend_icon = "📉"
trend_desc = "Strong downward trend"
elif last_val < first_val * 0.95:
trend_icon = "📊"
trend_desc = "Moderate downward trend"
else:
analysis.append("• ⚠️ Data is categorical — directional trend not meaningful")
trend_icon = "➡️"
trend_desc = "Relatively stable"
analysis.append(f"{trend_icon} {trend_desc}")
analysis.append(f"• Range: {min_val:.1f} to {max_val:.1f} (avg: {avg_val:.1f})")
# Volatility
+3 -132
View File
@@ -32,7 +32,6 @@ from urllib.parse import parse_qs, urlparse
from superset.constants import EXTRA_FORM_DATA_OVERRIDE_REGULAR_MAPPINGS
if TYPE_CHECKING:
from superset.mcp_service.chart.schemas import AppliedDashboardFilter
from superset.models.slice import Slice
logger = logging.getLogger(__name__)
@@ -45,33 +44,20 @@ QUERY_CONTEXT_EXTRA_FORM_DATA_OVERRIDE_KEYS = {
}
class ChartNotOnDashboardError(ValueError):
"""Raised when a chart is not part of the given dashboard's slices."""
def find_chart_by_identifier(
identifier: int | str,
query_options: list[Any] | None = None,
) -> Slice | None:
def find_chart_by_identifier(identifier: int | str) -> Slice | None:
"""Find a chart by numeric ID or UUID string.
Accepts an integer ID, a string that looks like a digit (e.g. "123"),
or a UUID string. Returns the Slice model instance or None.
``query_options`` is forwarded to the DAO so callers can eager-load
relationships needed after the request-scoped session is detached.
"""
from superset.daos.chart import ChartDAO # avoid circular import
extra: dict[str, Any] = (
{"query_options": query_options} if query_options is not None else {}
)
if isinstance(identifier, int) or (
isinstance(identifier, str) and identifier.isdigit()
):
chart_id = int(identifier) if isinstance(identifier, str) else identifier
return ChartDAO.find_by_id(chart_id, **extra)
return ChartDAO.find_by_id(identifier, id_column="uuid", **extra)
return ChartDAO.find_by_id(chart_id)
return ChartDAO.find_by_id(identifier, id_column="uuid")
def get_cached_form_data(form_data_key: str) -> str | None:
@@ -542,118 +528,3 @@ def extract_form_data_key_from_url(url: str | None) -> str | None:
parsed = urlparse(url)
values = parse_qs(parsed.query).get("form_data_key", [])
return values[0] if values else None
def _match_adhoc_by_subject(
adhoc_filters: Any, column: str | None
) -> tuple[str | None, Any] | None:
if not column or not isinstance(adhoc_filters, list):
return None
for af in adhoc_filters:
if isinstance(af, dict) and af.get("subject") == column:
return af.get("operator"), af.get("comparator")
return None
def _match_legacy_by_col(
legacy_filters: Any, column: str | None
) -> tuple[str | None, Any] | None:
if not column or not isinstance(legacy_filters, list):
return None
for f in legacy_filters:
if isinstance(f, dict) and f.get("col") == column:
return f.get("op"), f.get("val")
return None
def _resolve_filter_operator_and_value(
extra_form_data: dict[str, Any] | None,
column: str | None,
) -> tuple[str | None, Any]:
"""Pull operator and value for a dashboard filter from its
default extra_form_data, matching on target column where applicable."""
if not extra_form_data:
return None, None
if match := _match_adhoc_by_subject(extra_form_data.get("adhoc_filters"), column):
return match
if match := _match_legacy_by_col(extra_form_data.get("filters"), column):
return match
# Temporal filters contribute time_range with no target column
if time_range := extra_form_data.get("time_range"):
return "TIME_RANGE", time_range
return None, None
def build_applied_dashboard_filters(
dashboard_id: int, chart_id: int
) -> list[AppliedDashboardFilter]:
"""Resolve dashboard-level native filters in scope for a chart.
Validates that the dashboard exists, the caller has access, and the chart
is on the dashboard. Returns one AppliedDashboardFilter per non-DIVIDER
native filter whose scope includes the chart, populated with the filter's
default operator and value.
Raises DashboardNotFoundError if the dashboard is missing,
ChartNotOnDashboardError if the chart is not on it, and
SupersetSecurityException if the caller cannot access the dashboard.
"""
# Local imports avoid circular deps at module load
from superset import db, security_manager
from superset.charts.data.dashboard_filter_context import (
_extract_filter_extra_form_data,
_get_filter_target_column,
_is_filter_in_scope_for_chart,
)
from superset.commands.dashboard.exceptions import DashboardNotFoundError
from superset.mcp_service.chart.schemas import AppliedDashboardFilter
from superset.models.dashboard import Dashboard
from superset.utils import json
dashboard = db.session.query(Dashboard).filter_by(id=dashboard_id).one_or_none()
if not dashboard:
raise DashboardNotFoundError(dashboard_id=str(dashboard_id))
security_manager.raise_for_access(dashboard=dashboard)
slice_ids = {slc.id for slc in dashboard.slices}
if chart_id not in slice_ids:
raise ChartNotOnDashboardError(
f"Chart {chart_id} is not on dashboard {dashboard_id}"
)
metadata = json.loads(dashboard.json_metadata or "{}")
native_filter_config = metadata.get("native_filter_configuration", [])
if not isinstance(native_filter_config, list):
return []
position_json = json.loads(dashboard.position_json or "{}")
if not isinstance(position_json, dict):
position_json = {}
applied: list[AppliedDashboardFilter] = []
for flt in native_filter_config:
if not isinstance(flt, dict):
continue
if flt.get("type", "") == "DIVIDER":
continue
if not _is_filter_in_scope_for_chart(flt, chart_id, position_json):
continue
extra_form_data, status = _extract_filter_extra_form_data(flt)
column = _get_filter_target_column(flt)
operator, value = _resolve_filter_operator_and_value(extra_form_data, column)
applied.append(
AppliedDashboardFilter(
id=flt.get("id"),
name=flt.get("name"),
filter_type=flt.get("filterType"),
column=column,
operator=operator,
value=value,
status=status.value,
)
)
return applied
+3 -58
View File
@@ -32,7 +32,6 @@ from superset.mcp_service.chart.schemas import (
ChartCapabilities,
ChartSemantics,
ColumnRef,
CurrencyFormat,
FilterConfig,
HandlebarsChartConfig,
MixedTimeseriesChartConfig,
@@ -478,7 +477,6 @@ def map_table_config(config: TableChartConfig) -> Dict[str, Any]:
]
form_data["row_limit"] = config.row_limit
add_color_scheme(form_data, config.color_scheme)
return form_data
@@ -549,35 +547,7 @@ def add_legend_config(form_data: Dict[str, Any], config: XYChartConfig) -> None:
if not config.legend.show:
form_data["show_legend"] = False
if config.legend.position:
# Canonical form_data key is camelCase; the echarts plugins read
# `legendOrientation` directly off form_data.
form_data["legendOrientation"] = config.legend.position
def add_color_scheme(form_data: Dict[str, Any], color_scheme: str | None) -> None:
"""Add color scheme to form_data when set."""
if color_scheme:
form_data["color_scheme"] = color_scheme
def add_currency_format(
form_data: Dict[str, Any],
currency_format: CurrencyFormat | None,
key: str = "currency_format",
) -> None:
"""Add currency format to form_data under the given key when set."""
if currency_format:
form_data[key] = currency_format.to_form_data()
def add_xy_data_label_options(
form_data: Dict[str, Any], config: XYChartConfig, x_is_temporal: bool
) -> None:
"""Apply XY-specific data-label and time-format options when set."""
if config.x_axis_time_format and x_is_temporal:
form_data["x_axis_time_format"] = config.x_axis_time_format
if config.show_value:
form_data["show_value"] = True
form_data["legend_orientation"] = config.legend.position
def add_orientation_config(form_data: Dict[str, Any], config: XYChartConfig) -> None:
@@ -752,9 +722,6 @@ def map_xy_config(
add_axis_config(form_data, config)
add_legend_config(form_data, config)
add_orientation_config(form_data, config)
add_color_scheme(form_data, config.color_scheme)
add_currency_format(form_data, config.currency_format)
add_xy_data_label_options(form_data, config, x_is_temporal)
return form_data
@@ -767,13 +734,11 @@ def map_pie_config(config: PieChartConfig) -> Dict[str, Any]:
"viz_type": "pie",
"groupby": [config.dimension.name],
"metric": metric,
"color_scheme": config.color_scheme or "supersetColors",
"color_scheme": "supersetColors",
"show_labels": config.show_labels,
"show_legend": config.show_legend,
"legendOrientation": config.legend_orientation,
"label_type": config.label_type,
"number_format": config.number_format,
"date_format": config.date_format,
"sort_by_metric": config.sort_by_metric,
"row_limit": config.row_limit,
"donut": config.donut,
@@ -781,9 +746,9 @@ def map_pie_config(config: PieChartConfig) -> Dict[str, Any]:
"labels_outside": config.labels_outside,
"outerRadius": config.outer_radius,
"innerRadius": config.inner_radius,
"date_format": "smart_date",
}
add_currency_format(form_data, config.currency_format)
_add_adhoc_filters(form_data, config.filters)
return form_data
@@ -809,9 +774,6 @@ def map_big_number_config(config: BigNumberChartConfig) -> Dict[str, Any]:
if config.y_axis_format:
form_data["y_axis_format"] = config.y_axis_format
add_color_scheme(form_data, config.color_scheme)
add_currency_format(form_data, config.currency_format)
# Trendline-specific fields
if viz_type == "big_number":
# Big Number with trendline uses granularity_sqla for the temporal column
@@ -827,9 +789,6 @@ def map_big_number_config(config: BigNumberChartConfig) -> Dict[str, Any]:
if config.compare_lag is not None:
form_data["compare_lag"] = config.compare_lag
if config.time_format:
form_data["time_format"] = config.time_format
_add_adhoc_filters(form_data, config.filters)
return form_data
@@ -901,10 +860,6 @@ def map_pivot_table_config(config: PivotTableChartConfig) -> Dict[str, Any]:
"row_limit": config.row_limit,
}
if config.date_format:
form_data["date_format"] = config.date_format
add_currency_format(form_data, config.currency_format)
_add_adhoc_filters(form_data, config.filters)
return form_data
@@ -984,20 +939,10 @@ def map_mixed_timeseries_config(
"yAxisIndexB": 1,
# Display
"show_legend": config.show_legend,
"legendOrientation": config.legend_orientation,
"zoomable": True,
"rich_tooltip": True,
}
if config.show_value:
form_data["show_value"] = True
add_color_scheme(form_data, config.color_scheme)
add_currency_format(form_data, config.currency_format)
add_currency_format(
form_data, config.currency_format_secondary, key="currency_format_secondary"
)
# Configure temporal handling
configure_temporal_handling(form_data, x_is_temporal, config.time_grain)
-101
View File
@@ -36,10 +36,7 @@ import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Literal
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.exceptions import CommandException
from superset.errors import SupersetErrorType
from superset.mcp_service.chart.validation.dataset_validator import DatasetValidator
from superset.mcp_service.common.error_schemas import (
ChartGenerationError,
@@ -49,31 +46,6 @@ from superset.mcp_service.common.error_schemas import (
logger = logging.getLogger(__name__)
# Error types from db_engine_spec.extract_errors() that indicate a database
# connectivity or authentication issue rather than a query/config problem.
#
# GENERIC_DB_ENGINE_ERROR is included because many engines (BigQuery,
# Snowflake, Athena, Databricks, Trino) lack specific CONNECTION_* regex
# patterns in their engine specs — all their connection failures fall back
# to this generic type. This is safe here because _compile_chart only runs
# after Tier 1 schema validation has already verified columns, metrics, and
# filters against the dataset. At that point the SQL is auto-generated by
# Superset's query builder, so genuine SQL/config errors are very unlikely.
_CONNECTION_ERROR_TYPES = {
SupersetErrorType.CONNECTION_INVALID_USERNAME_ERROR,
SupersetErrorType.CONNECTION_INVALID_PASSWORD_ERROR,
SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR,
SupersetErrorType.CONNECTION_INVALID_PORT_ERROR,
SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR,
SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
SupersetErrorType.CONNECTION_DATABASE_PERMISSIONS_ERROR,
SupersetErrorType.CONNECTION_MISSING_PARAMETERS_ERROR,
SupersetErrorType.CONNECTION_DATABASE_TIMEOUT,
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
}
@dataclass
class CompileResult:
@@ -214,19 +186,6 @@ def _compile_chart(
return CompileResult(success=True, warnings=warnings, row_count=row_count)
except (ChartDataQueryFailedError, ChartDataCacheLoadError) as exc:
if _classify_as_database_error(exc, dataset_id):
logger.warning(
"Database connection error during chart compile check: %s: %s",
type(exc).__name__,
str(exc),
)
return CompileResult(
success=False,
error=f"Database connection error: {exc}",
error_code="CHART_COMPILE_FAILED",
tier="compile",
error_obj=_build_database_error(str(exc)),
)
return CompileResult(
success=False,
error=str(exc),
@@ -242,19 +201,6 @@ def _compile_chart(
tier="compile",
error_obj=_build_compile_error(str(exc)),
)
except SQLAlchemyError as exc:
logger.warning(
"Database connection error during chart compile check: %s: %s",
type(exc).__name__,
str(exc),
)
return CompileResult(
success=False,
error=f"Database connection error: {exc}",
error_code="CHART_COMPILE_FAILED",
tier="compile",
error_obj=_build_database_error(str(exc)),
)
def _adhoc_filter_column_valid(
@@ -332,53 +278,6 @@ def _validate_adhoc_filter_columns(
)
def _classify_as_database_error(exc: BaseException, dataset_id: int) -> bool:
"""Use the dataset's DB engine spec to classify the error.
Walks the ``__cause__`` chain for direct ``SQLAlchemyError`` instances,
then falls back to the engine spec's ``extract_errors`` regex patterns —
the same classification the Superset UI uses.
"""
# Direct SQLAlchemy errors (unwrapped or in cause chain)
current: BaseException | None = exc
while current is not None:
if isinstance(current, SQLAlchemyError):
return True
current = current.__cause__
# Use the dataset's engine spec to classify (same as the UI)
try:
from superset.daos.dataset import DatasetDAO
dataset = DatasetDAO.find_by_id(dataset_id)
if dataset and dataset.database and isinstance(exc, Exception):
errors = dataset.database.db_engine_spec.extract_errors(exc)
return any(e.error_type in _CONNECTION_ERROR_TYPES for e in errors)
except Exception: # pylint: disable=broad-except
logger.debug(
"Failed to classify error via engine spec for dataset %s: %s",
dataset_id,
exc,
)
return False
def _build_database_error(message: str) -> ChartGenerationError:
"""Wrap a database connection failure in the structured response envelope."""
return ChartGenerationError(
error_type="database_connection_error",
message="Unable to connect to the database.",
details=message or "",
suggestions=[
"Check that the database is online and reachable",
"Verify database credentials and connection settings",
"Contact your administrator if the issue persists",
],
error_code="DATABASE_CONNECTION_ERROR",
)
def _build_compile_error(message: str) -> ChartGenerationError:
"""Wrap a raw compile-failure string in the structured response envelope."""
return ChartGenerationError(
+10 -195
View File
@@ -23,7 +23,7 @@ from __future__ import annotations
import difflib
from datetime import datetime
from typing import Annotated, Any, cast, Dict, List, Literal, Protocol
from typing import Annotated, Any, Dict, List, Literal, Protocol
import humanize
from pydantic import (
@@ -146,7 +146,7 @@ class ChartInfo(BaseModel):
),
)
form_data_key: str | None = Field(
default=None,
None,
description=(
"Cache key used to retrieve unsaved form_data. When present, indicates "
"the form_data came from cache (unsaved edits) rather than the saved chart."
@@ -279,15 +279,6 @@ class GetChartInfoRequest(BaseModel):
"Can be used alone (without identifier) for unsaved charts."
),
)
dashboard_id: int | None = Field(
default=None,
description=(
"When provided, resolves dashboard-level native filters that are in "
"scope for this chart on the given dashboard and returns them under "
"filters.dashboard_filters. Requires the chart to be on the dashboard "
"and the caller to have dashboard access."
),
)
@model_validator(mode="after")
def validate_identifier_or_form_data_key(self) -> "GetChartInfoRequest":
@@ -532,18 +523,17 @@ class ChartFilter(ColumnOperator):
value: The value to filter by (type depends on col and opr).
"""
col: Literal[ # pyright: ignore[reportIncompatibleVariableOverride]
col: Literal[
"slice_name",
"viz_type",
"datasource_name",
"created_by_fk",
"changed_by_fk",
] = Field(
...,
description="Column to filter on. Use get_schema(model_type='chart') for "
"available filter columns. To filter by a person, first call find_users "
"to resolve a name to a user ID, then filter by created_by_fk or "
"changed_by_fk with that integer ID.",
description=(
"Column to filter on. Valid values: 'slice_name', 'viz_type', "
"'datasource_name'. Other column names are not valid filter columns "
"and will cause a validation error."
),
)
opr: ColumnOperatorEnum = Field(
...,
@@ -741,29 +731,6 @@ class LegendConfig(BaseModel):
position: Literal["top", "bottom", "left", "right"] | None = "right"
class CurrencyFormat(BaseModel):
"""Currency symbol and placement applied to numeric values."""
model_config = ConfigDict(populate_by_name=True)
symbol: str = Field(
...,
description="Currency code or symbol (e.g. 'USD', 'EUR', '$', '')",
max_length=20,
)
symbol_position: Literal["prefix", "suffix"] = Field(
"prefix",
description="Whether to render the symbol before or after the value",
validation_alias=AliasChoices("symbol_position", "symbolPosition"),
)
def to_form_data(self) -> Dict[str, str]:
return {"symbol": self.symbol, "symbolPosition": self.symbol_position}
LEGEND_POSITION_LITERAL = Literal["top", "bottom", "left", "right"]
class FilterConfig(BaseModel):
model_config = ConfigDict(populate_by_name=True)
@@ -890,27 +857,6 @@ class PieChartConfig(UnknownFieldCheckMixin):
)
row_limit: int = Field(100, description="Max slices", ge=1, le=10000)
number_format: str = Field("SMART_NUMBER", max_length=50)
date_format: str = Field(
"smart_date",
description="Date format for date dimension labels (e.g. 'smart_date', "
"'%Y-%m-%d')",
max_length=50,
)
currency_format: CurrencyFormat | None = Field(
None,
description="Currency symbol applied to the metric value",
)
color_scheme: str | None = Field(
None,
description=(
"Superset color scheme ID (e.g. 'supersetColors', 'lyftColors', "
"'googleCategory10c', 'd3Category10'). Defaults to 'supersetColors'."
),
max_length=100,
)
legend_orientation: LEGEND_POSITION_LITERAL = Field(
"top", description="Legend placement around the chart"
)
show_total: bool = Field(False, description="Show total in center")
labels_outside: bool = True
outer_radius: int = Field(70, description="Outer radius % (1-100)", ge=1, le=100)
@@ -962,15 +908,6 @@ class PivotTableChartConfig(UnknownFieldCheckMixin):
)
row_limit: int = Field(10000, description="Max cells", ge=1, le=50000)
value_format: str = Field("SMART_NUMBER", max_length=50)
date_format: str | None = Field(
None,
description="Date format for date columns (e.g. 'smart_date', '%Y-%m-%d')",
max_length=50,
)
currency_format: CurrencyFormat | None = Field(
None,
description="Currency symbol applied to numeric metric values",
)
class MixedTimeseriesChartConfig(UnknownFieldCheckMixin):
@@ -1017,29 +954,9 @@ class MixedTimeseriesChartConfig(UnknownFieldCheckMixin):
)
# Display options
show_legend: bool = True
legend_orientation: LEGEND_POSITION_LITERAL = Field(
"top", description="Legend placement around the chart"
)
show_value: bool = Field(False, description="Show data labels on each data point")
x_axis: AxisConfig | None = None
y_axis: AxisConfig | None = None
y_axis_secondary: AxisConfig | None = None
color_scheme: str | None = Field(
None,
description=(
"Superset color scheme ID (e.g. 'supersetColors', 'lyftColors'). "
"When omitted, Superset's default scheme is used."
),
max_length=100,
)
currency_format: CurrencyFormat | None = Field(
None,
description="Currency symbol applied to primary metric values",
)
currency_format_secondary: CurrencyFormat | None = Field(
None,
description="Currency symbol applied to secondary metric values",
)
filters: List[FilterConfig] | None = Field(
None,
description="Structured filters (column/op/value). "
@@ -1215,27 +1132,6 @@ class BigNumberChartConfig(UnknownFieldCheckMixin):
),
max_length=50,
)
time_format: str | None = Field(
None,
description=(
"Date format string for trendline x-axis labels "
"(e.g. 'smart_date', '%Y-%m-%d'). Only applies when "
"show_trendline=True."
),
max_length=50,
)
currency_format: CurrencyFormat | None = Field(
None,
description="Currency symbol applied to the metric value",
)
color_scheme: str | None = Field(
None,
description=(
"Superset color scheme ID for the trendline (e.g. 'supersetColors'). "
"When omitted, Superset's default scheme is used."
),
max_length=100,
)
start_y_axis_at_zero: bool = Field(
True,
description="Anchor trendline y-axis at zero",
@@ -1321,14 +1217,6 @@ class TableChartConfig(UnknownFieldCheckMixin):
validation_alias=AliasChoices("sort_by", "order_by_cols", "order_by"),
)
row_limit: int = Field(1000, description="Max rows returned", ge=1, le=50000)
color_scheme: str | None = Field(
None,
description=(
"Superset color scheme ID applied to conditional/cell formatting "
"(e.g. 'supersetColors')."
),
max_length=100,
)
@model_validator(mode="after")
def validate_unique_column_labels(self) -> "TableChartConfig":
@@ -1410,28 +1298,6 @@ class XYChartConfig(UnknownFieldCheckMixin):
x_axis: AxisConfig | None = None
y_axis: AxisConfig | None = None
legend: LegendConfig | None = None
x_axis_time_format: str | None = Field(
None,
description=(
"Date format for temporal x-axis labels (e.g. 'smart_date', "
"'%Y-%m-%d'). Only applies when the x-axis column is temporal."
),
max_length=50,
)
show_value: bool = Field(False, description="Show data labels on each data point")
currency_format: CurrencyFormat | None = Field(
None,
description="Currency symbol applied to metric values",
)
color_scheme: str | None = Field(
None,
description=(
"Superset color scheme ID (e.g. 'supersetColors', 'lyftColors', "
"'googleCategory10c', 'd3Category10'). When omitted, Superset's "
"default scheme is used."
),
max_length=100,
)
filters: List[FilterConfig] | None = Field(
None,
description="Structured filters (column/op/value). "
@@ -1548,10 +1414,7 @@ class ListChartsRequest(OwnedByMeMixin, CreatedByMeMixin, MetadataCacheControl):
"""
from superset.mcp_service.utils.schema_utils import parse_json_or_model_list
return cast(
List[ChartFilter],
parse_json_or_model_list(v, ChartFilter, "filters"),
)
return parse_json_or_model_list(v, ChartFilter, "filters")
@field_validator("select_columns", mode="before")
@classmethod
@@ -1712,14 +1575,7 @@ class GenerateChartRequest(QueryCacheControl):
class GenerateExploreLinkRequest(FormDataCacheControl):
dataset_id: int | str = Field(..., description="Dataset identifier (ID, UUID)")
config: ChartConfig | None = Field(
None,
description=(
"Chart configuration. Optional; omit to get a default "
"explore URL that opens the dataset in Superset without a "
"preconfigured chart."
),
)
config: ChartConfig = Field(..., description="Chart configuration")
class UpdateChartRequest(QueryCacheControl):
@@ -2208,38 +2064,6 @@ class AdhocFilter(BaseModel):
model_config = ConfigDict(extra="ignore")
class AppliedDashboardFilter(BaseModel):
"""A dashboard-level native filter resolved against a specific chart.
Returned when get_chart_info is called with a dashboard_id. Values come
from the filter's default state on the saved dashboard (not a permalink).
"""
id: str | None = Field(None, description="Native filter ID")
name: str | None = Field(None, description="Filter display name")
filter_type: str | None = Field(
None, description="Native filter type (e.g. filter_select, filter_range)"
)
column: str | None = Field(None, description="Target column the filter applies to")
operator: str | None = Field(
None,
description=(
"Filter operator as stored in extra_form_data (e.g. 'IN', '==', 'LIKE', "
"or 'TIME_RANGE' for temporal filters with no target column)"
),
)
value: Any | None = Field(
None, description="Filter value(s) from the default data mask"
)
status: str = Field(
...,
description=(
"Whether the filter contributes to the chart query: 'applied', "
"'not_applied', or 'not_applied_uses_default_to_first_item_prequery'"
),
)
class ChartFiltersInfo(BaseModel):
"""Structured representation of all filters applied to a chart."""
@@ -2281,15 +2105,6 @@ class ChartFiltersInfo(BaseModel):
None,
description="Custom HAVING clause applied to the chart query",
)
dashboard_filters: List[AppliedDashboardFilter] = Field(
default_factory=list,
description=(
"Dashboard-level native filters in scope for this chart on the "
"dashboard passed via get_chart_info's dashboard_id argument. Empty "
"when no dashboard_id was provided or no native filter targets this "
"chart."
),
)
# Rebuild ChartInfo so Pydantic can resolve the ChartFiltersInfo forward reference.
@@ -352,20 +352,6 @@ async def generate_chart( # noqa: C901
"Chart creation failed - no chart ID returned"
)
# Reload server-generated timestamps (created_on,
# changed_on) so the serializer sees real values.
from superset import db
try:
db.session.refresh(chart)
except SQLAlchemyError:
logger.warning(
"Chart %s created but refresh failed; "
"continuing with current values",
chart.id,
exc_info=True,
)
await ctx.info(
"Chart created successfully: chart_id=%s, chart_name=%s"
% (
@@ -416,7 +402,7 @@ async def generate_chart( # noqa: C901
)
execution_time = int((time.time() - start_time) * 1000)
error = compile_result.error_obj or ChartGenerationError(
error = ChartGenerationError(
error_type="compile_error",
message=(
"Chart query failed to execute. The chart was not saved."
@@ -546,7 +532,7 @@ async def generate_chart( # noqa: C901
)
execution_time = int((time.time() - start_time) * 1000)
error = compile_result.error_obj or ChartGenerationError(
error = ChartGenerationError(
error_type="compile_error",
message=(
"Chart query failed to execute. "
+15 -199
View File
@@ -26,7 +26,6 @@ from typing import Any, Dict, List, TYPE_CHECKING
from fastmcp import Context
from flask import current_app
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import subqueryload
from superset_core.mcp.decorators import tool, ToolAnnotations
if TYPE_CHECKING:
@@ -59,177 +58,9 @@ from superset.mcp_service.utils.oauth2_utils import (
build_oauth2_redirect_message,
OAUTH2_CONFIG_ERROR_MESSAGE,
)
from superset.utils.core import GenericDataType
logger = logging.getLogger(__name__)
_GENERIC_TYPE_MAP: dict[int, str] = {
GenericDataType.NUMERIC: "numeric",
GenericDataType.STRING: "string",
GenericDataType.TEMPORAL: "temporal",
GenericDataType.BOOLEAN: "boolean",
}
# Maps Superset viz_type strings to canonical categories so we can
# avoid recommending a chart type the user already has.
_VIZ_CATEGORY: dict[str, str] = {
"echarts_timeseries_line": "line",
"echarts_timeseries_smooth": "line",
"echarts_timeseries_step": "line",
"echarts_timeseries": "line",
"echarts_timeseries_bar": "bar",
"echarts_area": "area",
"echarts_timeseries_scatter": "scatter",
"mixed_timeseries": "line",
"table": "table",
"pie": "pie",
"big_number": "kpi",
"big_number_total": "kpi",
"pop_kpi": "kpi",
"dist_bar": "bar",
"line": "line",
"area": "area",
"scatter": "scatter",
"bubble": "bubble",
"treemap_v2": "treemap",
"sunburst_v2": "treemap",
"heatmap_v2": "heatmap",
"gauge_chart": "gauge",
"funnel": "funnel",
"histogram": "histogram",
"histogram_v2": "histogram",
"box_plot": "box_plot",
"world_map": "map",
"pivot_table_v2": "table",
}
_MAX_RECOMMENDATIONS = 4
def _recommend_visualizations(
viz_type: str,
columns: list[DataColumn],
row_count: int,
) -> list[str]:
"""Suggest visualization types based on column types,
cardinality, and the chart's current viz_type.
"""
if not columns:
return ["table"]
current_category = _VIZ_CATEGORY.get(viz_type, viz_type)
candidates = _build_candidates(columns, row_count)
if not candidates:
candidates = ["table", "bar chart"]
return _filter_candidates(candidates, current_category)
def _build_candidates(
columns: list[DataColumn],
row_count: int,
) -> list[str]:
"""Build candidate visualization list from column metadata."""
temporal = [c for c in columns if c.data_type == "temporal"]
numeric = [c for c in columns if c.data_type == "numeric"]
categorical = [c for c in columns if c.data_type in ("string", "boolean")]
if temporal and numeric:
return _candidates_temporal_numeric(numeric, row_count)
if categorical and numeric:
return _candidates_categorical_numeric(numeric, categorical)
if len(numeric) >= 2:
return _candidates_multi_numeric(numeric, categorical)
if len(numeric) == 1 and not temporal and not categorical:
return _candidates_single_numeric(numeric[0], row_count)
return []
def _candidates_temporal_numeric(
numeric: list[DataColumn], row_count: int
) -> list[str]:
# Few data points are better as a bar chart than a line
if row_count < 5:
candidates = ["bar chart", "table"]
else:
candidates = ["line chart", "area chart", "bar chart"]
if len(numeric) > 1:
candidates.append("multi-line chart")
return candidates
def _candidates_categorical_numeric(
numeric: list[DataColumn],
categorical: list[DataColumn],
) -> list[str]:
candidates = ["bar chart"]
if len(numeric) == 1 and categorical[0].unique_count <= 10:
candidates.append("pie chart")
if len(numeric) >= 2:
candidates.append("scatter plot")
candidates.append("heatmap")
if any(c.unique_count > 5 for c in categorical):
candidates.append("treemap")
return candidates
def _candidates_single_numeric(col: DataColumn, row_count: int) -> list[str]:
candidates = ["big number / KPI", "gauge chart"]
if row_count > 20 and col.unique_count > 10:
candidates.insert(0, "histogram")
return candidates
def _candidates_multi_numeric(
numeric: list[DataColumn],
categorical: list[DataColumn],
) -> list[str]:
candidates = ["scatter plot"]
if len(numeric) >= 3:
candidates.append("bubble chart")
if categorical:
candidates.append("heatmap")
return candidates
# Maps each candidate string to a canonical category for dedup
# against the current viz_type.
_CANDIDATE_CATEGORY: dict[str, str] = {
"line chart": "line",
"multi-line chart": "line",
"area chart": "area",
"bar chart": "bar",
"scatter plot": "scatter",
"bubble chart": "bubble",
"pie chart": "pie",
"treemap": "treemap",
"heatmap": "heatmap",
"big number / KPI": "kpi",
"gauge chart": "gauge",
"histogram": "histogram",
"table": "table",
}
def _filter_candidates(
candidates: list[str],
current_category: str,
) -> list[str]:
"""Deduplicate, exclude the current viz category, and cap."""
seen: set[str] = set()
result: list[str] = []
for c in candidates:
if c in seen:
continue
if _CANDIDATE_CATEGORY.get(c) == current_category:
continue
seen.add(c)
result.append(c)
if len(result) >= _MAX_RECOMMENDATIONS:
break
return result
def _sanitize_chart_data_for_llm_context(chart_data: ChartData) -> ChartData:
"""Wrap chart data read-path descriptive fields before LLM exposure."""
@@ -351,18 +182,7 @@ async def get_chart_data( # noqa: C901
# Build query context entirely from cached form_data
return await _query_from_form_data(cached_form_data_dict, request, ctx)
# Find the chart by identifier.
# Eagerly load the dataset's metrics relationship so Excel export
# (which may run after the request-scoped session is detached) can
# access dataset.metrics without triggering a lazy load. See
# apache/superset#39206 for the analogous database eager-load fix.
from superset.connectors.sqla.models import SqlaTable
from superset.models.slice import Slice
chart_query_options = [
subqueryload(Slice.table).subqueryload(SqlaTable.metrics),
]
# Find the chart by identifier
with event_logger.log_context(action="mcp.get_chart_data.chart_lookup"):
await ctx.debug("Looking up chart: identifier=%s" % (request.identifier,))
if request.identifier is None:
@@ -370,9 +190,7 @@ async def get_chart_data( # noqa: C901
error="Chart identifier is required",
error_type="ValidationError",
)
chart = find_chart_by_identifier(
request.identifier, query_options=chart_query_options
)
chart = find_chart_by_identifier(request.identifier)
if not chart:
await ctx.warning("Chart not found: identifier=%s" % (request.identifier,))
@@ -666,9 +484,8 @@ async def get_chart_data( # noqa: C901
)
# Create rich column metadata
coltypes = query_result.get("coltypes", [])
columns = []
for idx, col_name in enumerate(raw_columns):
for col_name in raw_columns:
# Sample some values for metadata
sample_values = [
row.get(col_name)
@@ -676,16 +493,13 @@ async def get_chart_data( # noqa: C901
if row.get(col_name) is not None
]
# Use SQL-derived GenericDataType when available,
# fall back to Python isinstance heuristic
# Infer data type
data_type = "string"
if coltypes:
data_type = _GENERIC_TYPE_MAP.get(coltypes[idx], "string")
elif sample_values:
if all(isinstance(v, bool) for v in sample_values):
data_type = "boolean"
elif all(isinstance(v, (int, float)) for v in sample_values):
if sample_values:
if all(isinstance(v, (int, float)) for v in sample_values):
data_type = "numeric"
elif all(isinstance(v, bool) for v in sample_values):
data_type = "boolean"
columns.append(
DataColumn(
@@ -728,11 +542,13 @@ async def get_chart_data( # noqa: C901
else:
insights.append("Fresh data retrieved from database")
recommended_visualizations = _recommend_visualizations(
viz_type=chart.viz_type or "unknown",
columns=columns,
row_count=len(data),
)
recommended_visualizations = []
if any(
"time" in col.lower() or "date" in col.lower() for col in raw_columns
):
recommended_visualizations.extend(["line chart", "time series"])
if len(raw_columns) <= 3:
recommended_visualizations.extend(["bar chart", "scatter plot"])
# Performance metadata with cache awareness
execution_time = int((time.time() - start_time) * 1000)
@@ -25,19 +25,12 @@ from fastmcp import Context
from sqlalchemy.orm import subqueryload
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.commands.dashboard.exceptions import DashboardNotFoundError
from superset.exceptions import SupersetSecurityException
from superset.extensions import event_logger
from superset.mcp_service.chart.chart_helpers import (
build_applied_dashboard_filters,
ChartNotOnDashboardError,
get_cached_form_data,
)
from superset.mcp_service.chart.chart_helpers import get_cached_form_data
from superset.mcp_service.chart.chart_utils import validate_chart_dataset
from superset.mcp_service.chart.schemas import (
CHART_FORM_DATA_EXCLUDED_FIELD_NAMES,
ChartError,
ChartFiltersInfo,
ChartInfo,
extract_filters_from_form_data,
GetChartInfoRequest,
@@ -96,66 +89,6 @@ FORM_DATA_OVERRIDE_EXCLUDED_FIELD_NAMES = (
)
async def _validate_chart_dataset_access(
result: ChartInfo, ctx: Context
) -> ChartError | None:
"""Validate that the chart's dataset is accessible to the current user.
Returns a ChartError if the dataset is not accessible, otherwise None.
Logs any non-fatal warnings (e.g., virtual dataset warnings) via ctx.
"""
from superset.daos.chart import ChartDAO
if not result.id:
return None
chart = ChartDAO.find_by_id(result.id)
if not chart:
return None
validation_result = validate_chart_dataset(chart, check_access=True)
if not validation_result.is_valid:
await ctx.warning(
"Chart found but dataset is not accessible: %s" % (validation_result.error,)
)
return ChartError(
error=validation_result.error or "Chart's dataset is not accessible",
error_type="DatasetNotAccessible",
)
for warning in validation_result.warnings:
await ctx.warning("Dataset warning: %s" % (warning,))
return None
async def _attach_dashboard_filters(
result: ChartInfo, dashboard_id: int, ctx: Context
) -> ChartError | None:
"""Resolve dashboard-scoped native filters and attach them to result.filters.
Returns a ChartError to surface to the caller on validation / access
failures, or None on success (including the no-filters case).
"""
if not result.id:
return None
with event_logger.log_context(action="mcp.get_chart_info.dashboard_filters"):
try:
dashboard_filters = build_applied_dashboard_filters(dashboard_id, result.id)
except DashboardNotFoundError as exc:
await ctx.warning("Dashboard not found: %s" % (str(exc),))
return ChartError(error=str(exc), error_type="DashboardNotFound")
except ChartNotOnDashboardError as exc:
await ctx.warning("Chart not on dashboard: %s" % (str(exc),))
return ChartError(error=str(exc), error_type="ChartNotOnDashboard")
except SupersetSecurityException as exc:
await ctx.warning("Dashboard not accessible: %s" % (str(exc),))
return ChartError(error=str(exc), error_type="DashboardNotAccessible")
if dashboard_filters:
if result.filters is None:
result.filters = ChartFiltersInfo(dashboard_filters=dashboard_filters)
else:
result.filters.dashboard_filters = dashboard_filters
return None
def _apply_unsaved_state_override(result: ChartInfo, form_data_key: str) -> None:
"""Override a ChartInfo's form_data with cached unsaved state."""
from superset.utils import json as utils_json
@@ -245,17 +178,6 @@ async def get_chart_info(
}
```
With dashboard context to resolve applied dashboard-level filters:
```json
{
"identifier": 123,
"dashboard_id": 45
}
```
When dashboard_id is provided, the response's filters.dashboard_filters
lists native filters (with column, operator, and value) that are in scope
for this chart on that dashboard.
Returns chart details including name, type, and URL.
"""
from superset.daos.chart import ChartDAO
@@ -325,14 +247,23 @@ async def get_chart_info(
)
# Validate the chart's dataset is accessible
dataset_error = await _validate_chart_dataset_access(result, ctx)
if dataset_error is not None:
return dataset_error
if request.dashboard_id:
error = await _attach_dashboard_filters(result, request.dashboard_id, ctx)
if error is not None:
return error
if result.id:
chart = ChartDAO.find_by_id(result.id)
if chart:
validation_result = validate_chart_dataset(chart, check_access=True)
if not validation_result.is_valid:
await ctx.warning(
"Chart found but dataset is not accessible: %s"
% (validation_result.error,)
)
return ChartError(
error=validation_result.error
or "Chart's dataset is not accessible",
error_type="DatasetNotAccessible",
)
# Log any warnings (e.g., virtual dataset warnings)
for warning in validation_result.warnings:
await ctx.warning("Dataset warning: %s" % (warning,))
else:
await ctx.warning("Chart retrieval failed: error=%s" % (str(result),))
@@ -104,16 +104,11 @@ async def list_charts(
list_charts(search="revenue", page=1) # DO NOT DO THIS
Valid filter columns for ``filters[].col``:
``slice_name``, ``viz_type``, ``datasource_name``,
``created_by_fk``, ``changed_by_fk``
``slice_name``, ``viz_type``, ``datasource_name``
Sortable columns for ``order_column``:
``id``, ``slice_name``, ``viz_type``, ``description``,
``changed_on``, ``created_on``
To filter by a person, call find_users to resolve the name to a user ID,
then pass it as a filter: filters=[{"col": "created_by_fk", "opr": "eq",
"value": <id>}] (or "changed_by_fk"). Do not pass the name as search.
"""
request = request or _DEFAULT_LIST_CHARTS_REQUEST.model_copy(deep=True)
await ctx.info(
@@ -37,12 +37,10 @@ class ColumnMetadata(BaseModel):
"""Metadata for a selectable column."""
name: str = Field(..., description="Column name to use in select_columns")
description: str | None = Field(default=None, description="Column description")
type: str | None = Field(
default=None, description="Data type (str, int, datetime, etc.)"
)
description: str | None = Field(None, description="Column description")
type: str | None = Field(None, description="Data type (str, int, datetime, etc.)")
is_default: bool = Field(
default=False, description="Whether this column is included by default"
False, description="Whether this column is included by default"
)
+8 -15
View File
@@ -67,7 +67,7 @@ from __future__ import annotations
import logging
from datetime import datetime
from typing import Annotated, Any, cast, Dict, List, Literal, TYPE_CHECKING
from typing import Annotated, Any, Dict, List, Literal, TYPE_CHECKING
import humanize
from pydantic import (
@@ -169,20 +169,16 @@ class DashboardFilter(ColumnOperator):
value: The value to filter by (type depends on col and opr).
"""
col: Literal[ # pyright: ignore[reportIncompatibleVariableOverride]
col: Literal[
"dashboard_title",
"published",
"favorite",
"created_by_fk",
"changed_by_fk",
] = Field(
...,
description=(
"Column to filter on. Use "
"get_schema(model_type='dashboard') for available "
"filter columns. To filter by a person, first call find_users to "
"resolve a name to a user ID, then filter by created_by_fk or "
"changed_by_fk with that integer ID."
"Column to filter on. Valid values: 'dashboard_title', 'published', "
"'favorite'. Other column names are not valid filter columns and will "
"cause a validation error."
),
)
opr: ColumnOperatorEnum = Field(
@@ -227,10 +223,7 @@ class ListDashboardsRequest(OwnedByMeMixin, CreatedByMeMixin, MetadataCacheContr
"""
from superset.mcp_service.utils.schema_utils import parse_json_or_model_list
return cast(
List[DashboardFilter],
parse_json_or_model_list(v, DashboardFilter, "filters"),
)
return parse_json_or_model_list(v, DashboardFilter, "filters")
@field_validator("select_columns", mode="before")
@classmethod
@@ -399,14 +392,14 @@ class DashboardInfo(BaseModel):
# Fields for permalink/filter state support
permalink_key: str | None = Field(
default=None,
None,
description=(
"Permalink key used to retrieve filter state. When present, indicates "
"the filter_state came from a permalink rather than the default dashboard."
),
)
filter_state: Dict[str, Any] | None = Field(
default=None,
None,
description=(
"Filter state from permalink. Contains dataMask (native filter values), "
"activeTabs, anchor, and urlParams. When present, represents the actual "
@@ -335,15 +335,6 @@ def generate_dashboard( # noqa: C901
db.session.add(dashboard)
db.session.commit() # pylint: disable=consider-using-transaction
try:
db.session.refresh(dashboard)
except SQLAlchemyError:
logger.warning(
"Dashboard %s created but refresh failed; "
"continuing with current values",
dashboard.id,
exc_info=True,
)
except SQLAlchemyError as db_err:
try:
db.session.rollback() # pylint: disable=consider-using-transaction
@@ -414,7 +405,6 @@ def generate_dashboard( # noqa: C901
# Convert to our response format
from superset.mcp_service.dashboard.schemas import (
_humanize_timestamp,
serialize_chart_summary,
serialize_tag_object,
)
@@ -428,10 +418,6 @@ def generate_dashboard( # noqa: C901
published=dashboard.published,
created_on=dashboard.created_on,
changed_on=dashboard.changed_on,
created_on_humanized=_humanize_timestamp(dashboard.created_on),
changed_on_humanized=_humanize_timestamp(dashboard.changed_on),
created_by=dashboard.created_by_name or None,
changed_by=dashboard.changed_by_name or None,
uuid=str(dashboard.uuid) if dashboard.uuid else None,
url=f"{get_superset_base_url()}/superset/dashboard/{dashboard.id}/",
chart_count=len(request.chart_ids),
@@ -98,18 +98,11 @@ async def list_dashboards(
list_dashboards(search="sales", page=1) # DO NOT DO THIS
Valid filter columns for ``filters[].col``:
``dashboard_title``, ``published``, ``favorite``,
``created_by_fk``, ``changed_by_fk``
``dashboard_title``, ``published``, ``favorite``
Sortable columns for ``order_column``:
``id``, ``dashboard_title``, ``slug``, ``published``,
``changed_on``, ``created_on``
To filter by a person (e.g. "dashboards Maxime is working on"), do NOT pass
the name as the search parameter search matches titles and slugs only.
Instead, call find_users to resolve the name to a user ID, then pass it as
a filter: filters=[{"col": "created_by_fk", "opr": "eq", "value": <id>}]
(or "changed_by_fk" for "last modified by").
"""
request = request or _DEFAULT_LIST_DASHBOARDS_REQUEST.model_copy(deep=True)
await ctx.info(
+3 -6
View File
@@ -22,7 +22,7 @@ Pydantic schemas for database-related responses
from __future__ import annotations
from datetime import datetime
from typing import Annotated, Any, cast, Dict, List, Literal
from typing import Annotated, Any, Dict, List, Literal
import humanize
from pydantic import (
@@ -58,7 +58,7 @@ class DatabaseFilter(ColumnOperator):
value: The value to filter by (type depends on col and opr).
"""
col: Literal[ # pyright: ignore[reportIncompatibleVariableOverride]
col: Literal[
"database_name",
"expose_in_sqllab",
"allow_file_upload",
@@ -242,10 +242,7 @@ class ListDatabasesRequest(CreatedByMeMixin, MetadataCacheControl):
@classmethod
def parse_filters(cls, v: Any) -> List[DatabaseFilter]:
"""Accept both JSON string and list of objects."""
return cast(
List[DatabaseFilter],
parse_json_or_model_list(v, DatabaseFilter, "filters"),
)
return parse_json_or_model_list(v, DatabaseFilter, "filters")
@field_validator("select_columns", mode="before")
@classmethod
+8 -9
View File
@@ -65,18 +65,17 @@ class DatasetFilter(ColumnOperator):
value: The value to filter by (type depends on col and opr).
"""
col: Literal[ # pyright: ignore[reportIncompatibleVariableOverride]
col: Literal[
"table_name",
"schema",
"database_name",
"created_by_fk",
"changed_by_fk",
] = Field(
...,
description="Column to filter on. Use get_schema(model_type='dataset') for "
"available filter columns. To filter by a person, first call find_users "
"to resolve a name to a user ID, then filter by created_by_fk or "
"changed_by_fk with that integer ID.",
description=(
"Column to filter on. Valid values: 'table_name', 'schema', "
"'database_name'. Other column names (e.g. 'created_by_fk', 'id') "
"are not valid filter columns and will cause a validation error."
),
)
opr: ColumnOperatorEnum = Field(
...,
@@ -659,7 +658,7 @@ def serialize_dataset_object(dataset: Any) -> DatasetInfo | None:
params = None
columns = [
TableColumnInfo(
column_name=getattr(col, "column_name", None) or "",
column_name=getattr(col, "column_name", None),
verbose_name=getattr(col, "verbose_name", None),
type=getattr(col, "type", None),
is_dttm=getattr(col, "is_dttm", None),
@@ -671,7 +670,7 @@ def serialize_dataset_object(dataset: Any) -> DatasetInfo | None:
]
metrics = [
SqlMetricInfo(
metric_name=getattr(metric, "metric_name", None) or "",
metric_name=getattr(metric, "metric_name", None),
verbose_name=getattr(metric, "verbose_name", None),
expression=getattr(metric, "expression", None),
description=getattr(metric, "description", None),
@@ -109,15 +109,10 @@ async def list_datasets(
list_datasets(search="sales", page=1) # DO NOT DO THIS
Valid filter columns for ``filters[].col``:
``table_name``, ``schema``, ``database_name``,
``created_by_fk``, ``changed_by_fk``
``table_name``, ``schema``, ``database_name``
Sortable columns for ``order_column``:
``id``, ``table_name``, ``schema``, ``changed_on``, ``created_on``
To filter by a person, call find_users to resolve the name to a user ID,
then pass it as a filter: filters=[{"col": "created_by_fk", "opr": "eq",
"value": <id>}] (or "changed_by_fk"). Do not pass the name as search.
"""
if ctx is None:
raise RuntimeError("FastMCP context is required for list_datasets")
@@ -65,12 +65,10 @@ async def generate_explore_link(
- "Visualize [data]"
- General data exploration
- When user wants to SEE data visually
- Opening a dataset in Explore without a preconfigured chart (omit config)
IMPORTANT:
- Use numeric dataset ID or UUID (NOT schema.table_name format)
- When config is provided, MUST include chart_type (e.g. 'xy' or 'table')
- Omit config entirely to return a default explore URL for the dataset
- MUST include chart_type in config (either 'xy' or 'table')
Example usage:
```json
@@ -85,11 +83,6 @@ async def generate_explore_link(
}
```
Or with no config to simply open the dataset in Explore:
```json
{"dataset_id": 123}
```
Better UX because:
- Users can interact with chart before saving
- Easy to modify parameters instantly
@@ -100,10 +93,9 @@ async def generate_explore_link(
Returns explore URL for immediate use.
"""
chart_type = request.config.chart_type if request.config else "none"
await ctx.info(
"Generating explore link for dataset_id=%s, chart_type=%s"
% (request.dataset_id, chart_type)
% (request.dataset_id, request.config.chart_type)
)
await ctx.debug(
"Configuration details: use_cache=%s, force_refresh=%s, cache_form_data=%s"
@@ -111,6 +103,9 @@ async def generate_explore_link(
)
try:
# config is already a typed ChartConfig (validated by Pydantic)
config = request.config
await ctx.report_progress(1, 4, "Validating dataset exists")
with event_logger.log_context(action="mcp.generate_explore_link.dataset_check"):
from superset.daos.dataset import DatasetDAO
@@ -162,32 +157,8 @@ async def generate_explore_link(
),
}
# When no config is provided, return a default explore URL that opens
# the dataset in Superset without a preconfigured chart.
if request.config is None:
await ctx.report_progress(4, 4, "URL generation complete")
from superset.mcp_service.utils.url_utils import get_superset_base_url
base_url = get_superset_base_url()
default_url = (
f"{base_url}/explore/?datasource_type=table&datasource_id={dataset.id}"
)
await ctx.info(
"Default explore link generated: dataset_id=%s" % (request.dataset_id,)
)
return {
"url": default_url,
"form_data": {},
"form_data_key": None,
"chart_type_label": None,
"error": None,
}
await ctx.report_progress(2, 4, "Converting configuration to form data")
with event_logger.log_context(action="mcp.generate_explore_link.form_data"):
# config is already a typed ChartConfig (validated by Pydantic)
config = request.config
# Normalize column names to match canonical dataset column names
# This fixes case sensitivity issues (e.g., 'order_date' vs 'OrderDate')
try:
@@ -285,7 +256,7 @@ async def generate_explore_link(
"Explore link generation failed for dataset_id=%s, chart_type=%s: %s: %s"
% (
request.dataset_id,
chart_type,
request.config.chart_type,
type(e).__name__,
str(e),
)
+10 -5
View File
@@ -34,7 +34,6 @@ from superset.mcp_service.privacy import (
filter_user_directory_columns,
SELF_REFERENCING_FILTER_COLUMNS,
USER_DIRECTORY_FIELDS,
USER_FILTER_FIELDS,
)
from superset.mcp_service.system.schemas import PaginationInfo
from superset.mcp_service.utils import _is_uuid
@@ -315,6 +314,14 @@ class ModelListCore(BaseCore, Generic[L]):
has_previous=page > 0,
)
# Build response
def get_keys(obj: BaseModel | dict[str, Any] | Any) -> List[str]:
if hasattr(obj, "model_dump"):
return list(obj.model_dump().keys())
elif isinstance(obj, dict):
return list(obj.keys())
return []
response_kwargs = {
self.list_field_name: item_objs,
"count": len(item_objs),
@@ -588,7 +595,7 @@ class InstanceInfoCore(BaseCore):
return counts
def _calculate_time_based_metrics(
self, _base_counts: Dict[str, int]
self, base_counts: Dict[str, int]
) -> Dict[str, Dict[str, int]]:
"""Calculate time-based metrics for recent activity."""
now = datetime.now(timezone.utc)
@@ -767,9 +774,7 @@ class ModelGetSchemaCore(BaseCore, Generic[S]):
self.default_sort = default_sort
self.default_sort_direction = default_sort_direction
self.exclude_filter_columns = set(exclude_filter_columns or set())
# Hide user-directory columns from filter discovery, except the small
# set callers may legitimately filter by ID (resolved via find_users).
self.exclude_filter_columns.update(USER_DIRECTORY_FIELDS - USER_FILTER_FIELDS)
self.exclude_filter_columns.update(USER_DIRECTORY_FIELDS)
def _get_filter_columns(self) -> Dict[str, List[str]]:
"""Get filterable columns and operators from the DAO."""
+1 -15
View File
@@ -16,7 +16,6 @@
# under the License.
import logging
import secrets
import time
from collections import defaultdict
from typing import Any, Awaitable, Callable, Dict, Protocol, Sequence
@@ -246,20 +245,11 @@ class LoggingMiddleware(Middleware):
)
tool_name = getattr(context.message, "name", None)
mcp_call_id = secrets.token_hex(16)
context.mcp_call_id = mcp_call_id
start_time = time.time()
success = False
try:
result = await call_next(context)
success = not self._is_error_response(result)
if isinstance(result, ToolResult):
existing_meta = result.meta or {}
result = ToolResult(
content=result.content,
meta={**existing_meta, "mcp_call_id": mcp_call_id},
structured_content=result.structured_content,
)
return result
except Exception:
success = False
@@ -275,7 +265,6 @@ class LoggingMiddleware(Middleware):
slice_id=slice_id,
referrer=None,
curated_payload={
"mcp_call_id": mcp_call_id,
"tool": tool_name,
"agent_id": agent_id,
"params": _sanitize_params(params),
@@ -289,7 +278,7 @@ class LoggingMiddleware(Middleware):
logger.info(
"MCP tool call: tool=%s, agent_id=%s, user_id=%s, method=%s, "
"dashboard_id=%s, slice_id=%s, dataset_id=%s, duration_ms=%s, "
"success=%s, mcp_call_id=%s",
"success=%s",
tool_name,
agent_id,
user_id,
@@ -299,7 +288,6 @@ class LoggingMiddleware(Middleware):
dataset_id,
duration_ms,
success,
mcp_call_id,
)
async def on_message(
@@ -403,10 +391,8 @@ class StructuredContentStripperMiddleware(Middleware):
# unhandled exception — including ToolError from
# GlobalErrorHandlerMiddleware, ValueError, TypeError, etc. —
# will cause encoding failures on the wire.
mcp_call_id = getattr(context, "mcp_call_id", None)
return ToolResult(
content=[mt.TextContent(type="text", text=f"Error: {e}")],
meta={"mcp_call_id": mcp_call_id} if mcp_call_id else None,
)
if isinstance(result, ToolResult) and result.structured_content is not None:
result = ToolResult(content=result.content, meta=result.meta)
+3 -10
View File
@@ -44,20 +44,13 @@ USER_DIRECTORY_FIELDS = frozenset(
}
)
# User-directory columns that may be used as filter values (an integer user ID).
# These remain stripped from select_columns, sort, search, and tool responses
# (so the directory itself is never exposed), but list tools may filter rows by
# them when the caller already has an ID — typically resolved via find_users.
USER_FILTER_FIELDS = frozenset({"created_by_fk", "changed_by_fk"})
# Internal DAO filter column names generated server-side when translating the
# created_by_me / owned_by_me boolean flags (see mcp_core._prepend_self_lookup_filters).
# These columns are never exposed to LLM callers; they are excluded from the
# filters_applied response field to avoid leaking internal implementation details.
# Note: ``created_by_fk`` is intentionally excluded — it is also a publicly
# advertised filter column (see USER_FILTER_FIELDS) so callers can filter by a
# user ID resolved via find_users.
SELF_REFERENCING_FILTER_COLUMNS = frozenset({"owner", "created_by_fk_or_owner"})
SELF_REFERENCING_FILTER_COLUMNS = frozenset(
{"created_by_fk", "owner", "created_by_fk_or_owner"}
)
DATA_MODEL_METADATA_ACCESS_ATTR = "_requires_data_model_metadata_access"
DATA_MODEL_METADATA_ERROR_TYPE = "DataModelMetadataRestricted"
@@ -50,7 +50,6 @@ from superset.mcp_service.utils.oauth2_utils import (
build_oauth2_redirect_message,
OAUTH2_CONFIG_ERROR_MESSAGE,
)
from superset.sql.parse import SQLScript
logger = logging.getLogger(__name__)
@@ -118,50 +117,7 @@ async def execute_sql(request: ExecuteSqlRequest, ctx: Context) -> ExecuteSqlRes
error_type=SupersetErrorType.DATABASE_SECURITY_ACCESS_ERROR.value,
)
# 2. Block destructive DDL (DROP, TRUNCATE, ALTER)
# Fail-closed: if parsing fails, block the query rather than
# allowing potentially destructive SQL to bypass the check.
# Render Jinja2 templates first so templated SQL can be parsed.
with event_logger.log_context(action="mcp.execute_sql.ddl_check"):
try:
sql_to_check = request.sql
if request.template_params:
from superset.jinja_context import get_template_processor
tp = get_template_processor(database=database)
sql_to_check = tp.process_template(
request.sql, **request.template_params
)
script = SQLScript(sql_to_check, database.db_engine_spec.engine)
if script.has_destructive():
await ctx.error(
"Destructive DDL blocked: sql_preview=%r" % sql_preview
)
return ExecuteSqlResponse(
success=False,
error=(
"Destructive DDL statements (DROP, TRUNCATE, ALTER) "
"are not allowed through MCP. Use the Superset SQL "
"Lab UI for administrative database operations."
),
error_type=SupersetErrorType.DML_NOT_ALLOWED_ERROR.value,
)
except Exception as parse_err:
await ctx.error(
"DDL pre-check failed to parse SQL, blocking query: %s"
% str(parse_err)
)
return ExecuteSqlResponse(
success=False,
error=(
"SQL could not be parsed for security validation. "
"Please check your SQL syntax and try again."
),
error_type=SupersetErrorType.INVALID_SQL_ERROR.value,
)
# 3. Build QueryOptions and execute query
# 2. Build QueryOptions and execute query
cache_opts = CacheOptions(force_refresh=True) if request.force_refresh else None
options = QueryOptions(
catalog=request.catalog,
@@ -173,11 +129,11 @@ async def execute_sql(request: ExecuteSqlRequest, ctx: Context) -> ExecuteSqlRes
cache=cache_opts,
)
# 4. Execute query
# 3. Execute query
with event_logger.log_context(action="mcp.execute_sql.query_execution"):
result = database.execute(request.sql, options)
# 5. Convert to MCP response format
# 4. Convert to MCP response format
with event_logger.log_context(action="mcp.execute_sql.response_conversion"):
response = _convert_to_response(result)
+2 -90
View File
@@ -25,11 +25,9 @@ system-level info.
from __future__ import annotations
from datetime import datetime
from typing import Annotated, Any, List
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
from superset.mcp_service.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
from pydantic import BaseModel, ConfigDict, Field
class HealthCheckResponse(BaseModel):
@@ -172,84 +170,6 @@ def serialize_user_object(user: Any) -> UserInfo | None:
)
class FindUsersRequest(BaseModel):
"""Request schema for find_users tool.
Resolves a person's name (or partial name, username, or email) to user IDs
so they can be passed to listing tools as filter values for created_by_fk
or changed_by_fk. This is the only sanctioned path for "show me what
<person> is working on" queries.
"""
model_config = ConfigDict(extra="forbid")
query: Annotated[
str,
Field(
min_length=1,
max_length=200,
description=(
"Substring to match (case-insensitive) against username, "
"first_name, last_name, and email. Required and non-empty: "
"this tool does not enumerate the full user directory."
),
),
]
page_size: Annotated[
int,
Field(
default=DEFAULT_PAGE_SIZE,
gt=0,
le=MAX_PAGE_SIZE,
description=f"Maximum number of matches to return (max {MAX_PAGE_SIZE}).",
),
]
@field_validator("query")
@classmethod
def _reject_blank_query(cls, value: str) -> str:
# min_length=1 alone admits whitespace-only strings, which strip to "" and
# produce a "%%" LIKE pattern that matches every user. Strip and require
# at least one non-space character.
stripped = value.strip()
if not stripped:
raise ValueError("query must contain at least one non-whitespace character")
return stripped
class UserMatch(BaseModel):
"""Minimal user projection returned by find_users.
Intentionally narrower than UserInfo: only the fields needed to disambiguate
matches and pass an id to created_by_fk / changed_by_fk filters. Email,
active flag, and roles are deliberately excluded to limit identity
exposure through this directory-resolution path.
"""
id: int | None = None
username: str | None = None
first_name: str | None = None
last_name: str | None = None
class FindUsersResponse(BaseModel):
"""Response schema for find_users tool."""
users: List[UserMatch] = Field(
default_factory=list,
description=(
"Matching users. Pass user.id as the value for created_by_fk or "
"changed_by_fk filters on list_dashboards, list_charts, and "
"list_datasets."
),
)
count: int = Field(..., description="Number of users returned in this response.")
truncated: bool = Field(
default=False,
description="True when the query matched more rows than page_size allows.",
)
class TagInfo(BaseModel):
id: int | None = None
name: str | None = None
@@ -321,14 +241,6 @@ class GenerateBugReportRequest(BaseModel):
"PII and secrets are sanitized before being written to the report."
),
)
mcp_call_id: str | None = Field(
None,
description=(
"Optional MCP call ID from a previous tool invocation. "
"When provided, it will be included in the bug report "
"for server-side log correlation."
),
)
class GenerateBugReportResponse(BaseModel):
@@ -17,14 +17,12 @@
"""System tools for MCP service."""
from .find_users import find_users
from .generate_bug_report import generate_bug_report
from .get_instance_info import get_instance_info
from .get_schema import get_schema
from .health_check import health_check
__all__ = [
"find_users",
"generate_bug_report",
"health_check",
"get_instance_info",

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