mirror of
https://github.com/apache/superset.git
synced 2026-09-01 04:51:23 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bda849d9c | ||
|
|
cd8d1c1c92 | ||
|
|
00d75f962b | ||
|
|
3fb7c26b66 | ||
|
|
41bf352f95 |
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <=4.0x, users installed Firefox or Chrome and that was documented here.
|
||||
|
||||
|
||||
+3
-3
@@ -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": [
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
Generated
+247
-242
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
-20
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
+67
-38
@@ -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,
|
||||
|
||||
+1
-33
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+146
-146
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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. "
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user