Compare commits

..
Author SHA1 Message Date
Enzo Martellucci b2acbd286a fix(chart): respect ALLOW_FULL_CSV_EXPORT and fix streaming CSV export 2026-05-21 15:35:49 +02:00
Enzo Martellucci e8317b15e2 refactor(chart): rename fullCsvMaxRows to fullExportMaxRows 2026-05-21 14:38:53 +02:00
Enzo Martellucci 584917b467 fix(chart): respect ALLOW_FULL_CSV_EXPORT and fix streaming CSV export 2026-05-21 14:25:32 +02:00
dependabot[bot] a183582291 chore(deps): bump markdown-to-jsx from 9.8.0 to 9.8.1 in /superset-frontend (#40316)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 22:33:08 -07:00
dependabot[bot] 3acef94ef6 chore(deps): update zod requirement from ^4.4.1 to ^4.4.3 in /superset-frontend/plugins/plugin-chart-echarts (#40313)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 22:32:46 -07:00
dependabot[bot] 9638eecdb1 chore(deps-dev): bump oxlint from 1.65.0 to 1.66.0 in /superset-frontend (#40318)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 22:29:45 -07:00
Evan RusackasandClaude 7e74fc4192 fix(charts): handle PostgreSQL INTERVAL type in bar and pie charts (#34513)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 22:26:59 -07:00
Evan RusackasandClaude Opus 4.7 cdca6f7fdc fix(sqllab): keep saved-query list working when Jinja dataset(id) references a deleted dataset (#39703)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:19:19 -07:00
b1ca8cac6b fix(tests): fix flaky FileHandler test by awaiting LaunchQueue consumer in afterEach (#39508)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: sadpandajoe <jcli38@gmail.com>
2026-05-20 19:31:01 -07:00
Evan RusackasandClaude Code 2cd5efa627 ci(deps): bump lower bound on pip dependabot PRs (#40308)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-21 08:53:57 +07:00
a273fe4d62 fix(list-view): preserve user name in filter pill after navigation (#39505)
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:54:49 -07:00
d203f0de33 chore(sql-lab): finish SqlLab typed-dispatch migration for SaveDatasetModal (#40040)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 16:04:38 -07:00
a75f9b67b2 chore(superset-ui-switchboard): forward-compat fixes for TypeScript 6.0 (Phase E) (#40028)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:37:52 -07:00
Evan RusackasandClaude Code 3f0858e35d chore(sql-lab): migrate useDispatch to useAppDispatch (#40037)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 15:36:27 -07:00
Beto Dealmeida 68c145adc3 feat(semantic layers): add metadata on additive metrics (#40279) 2026-05-20 18:29:28 -04:00
4a9aecda4a fix(dashboard-import): remap chartsInScope on import (#26338) (#40140)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 13:41:14 -07:00
Evan RusackasandClaude Code 46b2d7d7a9 test(dashboard-import): pin native filter scope rootPath preservation (#19944) (#40135)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 12:25:34 -07:00
Evan RusackasandClaude Code f8600471fa test(datasets): regression test for Jinja not rendered on sync columns (#25839) (#40224)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 11:46:36 -07:00
Evan RusackasandClaude Code b23c65e04f test(charts): regression for last-modified sort order (#27500) (#40231)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 11:43:19 -07:00
Evan RusackasandClaude Code aa8255c55c test(reports): regression for alerts CSV missing chart time filters (#25538) (#40232)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 11:42:52 -07:00
Evan RusackasandClaude Code 10b7bfc8c1 test(helpers): regression for humanize locale activation (#28331) (#40233)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 11:42:17 -07:00
Evan RusackasandClaude 89cab1860e chore(codeowners): add @rusackas as translations maintainer (#40295)
Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 11:41:58 -07:00
dependabot[bot] b7585122c8 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.59.3 to 8.59.4 in /superset-websocket (#40250)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 10:45:00 -07:00
dependabot[bot]andClaude f2d80a183e chore(deps): bump content-disposition from 1.1.0 to 2.0.0 in /superset-frontend (#40109)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 10:41:34 -07:00
ShaitanandClaude Sonnet 4.6 69adecd6a3 fix(reports): enforce server-side recipient on chart/dashboard report subscriptions (#38847)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:36:42 -07:00
fbffae0444 fix(dataset-editor): drop null warning_markdown from extra JSON serialisation (#39706)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:34:03 -07:00
6ce7c2e8de chore(deps-dev): bump react-resizable and @types/react-resizable in /superset-frontend (#40110)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:32:38 -07:00
SkinnyPigeon 105820f1f4 docs(reports): playwright setup clarification (#40168) 2026-05-20 10:32:02 -07:00
Evan RusackasandClaude Code 92b1b0a219 ci(docs): soft-fail badge localization on transient fetch errors (#40236)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 10:21:58 -07:00
Evan RusackasandClaude Code c39a47cbac test(sql-parser): pin WITH+UNION as non-mutating across dialects (#25659) (#40138)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 10:08:21 -07:00
dependabot[bot] dacda71f77 chore(deps-dev): bump typescript-eslint from 8.59.3 to 8.59.4 in /superset-websocket (#40251)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 09:57:53 -07:00
dependabot[bot] 12a21c8933 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.59.3 to 8.59.4 in /superset-frontend (#40256)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 09:57:01 -07:00
dependabot[bot] 13fa3810a8 chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40262)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 09:56:40 -07:00
dependabot[bot] 3356f4d3e1 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40265)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 09:56:17 -07:00
dependabot[bot] 4a17c49d74 chore(deps): bump zod from 4.4.1 to 4.4.3 in /superset-frontend (#40272)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 09:55:49 -07:00
dependabot[bot] ea1ce7140c chore(deps-dev): bump webpack from 5.106.2 to 5.107.0 in /docs (#40291)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 22:35:32 +07:00
dependabot[bot] 038414ea5c chore(deps-dev): bump ts-jest from 29.4.9 to 29.4.10 in /superset-websocket (#40290)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 22:34:25 +07:00
jesperct 5bb54cc96b fix(echarts): preserve dataZoom range across setOption(notMerge) (#40173) 2026-05-20 17:33:29 +02:00
Alexandru Soare fb276b08dd fix(mcp): Skip misleading trend analysis for categorical ASCII charts (#39761) 2026-05-20 18:04:21 +03:00
Alexandru Soare 6e8b3bf976 fix(mcp): raise right error (#39964) 2026-05-20 14:32:45 +03:00
Alexandru Soare 55024e8f4d feat(mcp): Add mcp_call_id to tool responses for server log correlation (#39776) 2026-05-20 14:30:22 +03:00
Alexandru Soare b98bd2a07a fix(mcp): Block destructive DDL (DROP, TRUNCATE, ALTER) in execute_sql (#39621) 2026-05-20 14:29:15 +03:00
Alexandru Soare 0a3a35018c fix(mcp): changed_on_humanized null in write tool responses (generate_dashboard, generate_chart) (#39488) 2026-05-20 14:08:51 +03:00
Jean Massucatto e6179036ec fix(sqllab): handle scientific notation in big number JSON responses (#39994) 2026-05-20 07:39:47 +02:00
dependabot[bot] 81b4d580db chore(deps-dev): bump @types/node from 25.8.0 to 25.9.1 in /superset-websocket (#40249)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 21:54:36 -07:00
dependabot[bot] 9acfac1523 chore(deps-dev): bump @typescript-eslint/parser from 8.59.3 to 8.59.4 in /superset-websocket (#40252)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 21:53:59 -07:00
dependabot[bot] aa9af6c307 chore(deps-dev): bump typescript-eslint from 8.59.3 to 8.59.4 in /docs (#40254)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 21:52:10 -07:00
dependabot[bot] fbb3056508 chore(deps): bump baseline-browser-mapping from 2.10.30 to 2.10.31 in /docs (#40255)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 21:51:56 -07:00
dependabot[bot] ffbce27c9b chore(deps): bump codecov/codecov-action from 6.0.0 to 6.0.1 (#40269)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 21:51:40 -07:00
dependabot[bot] fe8b218a5f chore(deps): bump mapbox-gl from 3.23.1 to 3.24.0 in /superset-frontend (#40258)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 21:50:31 -07:00
dependabot[bot] f5fe9bfa26 chore(deps-dev): bump ts-jest from 29.4.9 to 29.4.10 in /superset-frontend (#40260)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 21:50:16 -07:00
dependabot[bot] 7f1c47521e chore(deps-dev): bump @typescript-eslint/parser from 8.59.3 to 8.59.4 in /superset-frontend (#40263)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 21:50:03 -07:00
dependabot[bot] 0fffa74bc6 chore(deps-dev): bump tsx from 4.22.0 to 4.22.3 in /superset-frontend (#40267)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 21:49:40 -07:00
dependabot[bot] 738ebf9cc6 chore(deps-dev): bump @types/node from 25.8.0 to 25.9.1 in /superset-frontend (#40268)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 21:49:26 -07:00
dependabot[bot] 98dff2e170 chore(deps): bump yeoman-generator from 8.1.2 to 8.2.2 in /superset-frontend (#40271)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 20:03:44 -07:00
Evan RusackasandClaude Code b5ad4a7a07 test(sql-parser): pin TimescaleDB hyperfunctions parse on postgresql (#32028) (#40142)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-19 19:53:33 -07:00
109 changed files with 3602 additions and 1617 deletions
+1 -1
View File
@@ -38,7 +38,7 @@
# Notify translation maintainers of changes to translations
/superset/translations/ @sfirke
/superset/translations/ @sfirke @rusackas
# Notify PMC members of changes to extension-related files
+5
View File
@@ -62,6 +62,11 @@ 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@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
with:
file: ./coverage.xml
flags: superset-extensions-cli
+1 -1
View File
@@ -128,7 +128,7 @@ jobs:
run: npx nyc merge coverage/ merged-output/coverage-summary.json
- name: Upload Code Coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # 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@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # 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@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # 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@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # 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@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # 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@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # 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@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # 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 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`.
- 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`.
In Superset versions &lt;=4.0x, users installed Firefox or Chrome and that was documented here.
+3 -3
View File
@@ -72,7 +72,7 @@
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.33",
"antd": "^6.4.3",
"baseline-browser-mapping": "^2.10.30",
"baseline-browser-mapping": "^2.10.31",
"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.3",
"webpack": "^5.106.2"
"typescript-eslint": "^8.59.4",
"webpack": "^5.107.0"
},
"browserslist": {
"production": [
+19 -5
View File
@@ -206,12 +206,26 @@ async function downloadBadge(url, staticDir) {
badgeCache.set(url, webPath);
return webPath;
} catch (error) {
// 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.`,
// 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.`,
);
badgeCache.set(url, url);
return url;
} finally {
// Clean up the in-flight tracker
inFlightDownloads.delete(url);
+110 -127
View File
@@ -4455,22 +4455,6 @@
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"
@@ -4612,7 +4596,7 @@
resolved "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.4.tgz"
integrity sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==
"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
"@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==
@@ -4828,100 +4812,100 @@
dependencies:
"@types/yargs-parser" "*"
"@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==
"@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==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@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"
"@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"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@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==
"@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==
dependencies:
"@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"
"@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"
debug "^4.4.3"
"@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==
"@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==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.59.3"
"@typescript-eslint/types" "^8.59.3"
"@typescript-eslint/tsconfig-utils" "^8.59.4"
"@typescript-eslint/types" "^8.59.4"
debug "^4.4.3"
"@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==
"@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==
dependencies:
"@typescript-eslint/types" "8.59.3"
"@typescript-eslint/visitor-keys" "8.59.3"
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/visitor-keys" "8.59.4"
"@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/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/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==
"@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==
dependencies:
"@typescript-eslint/types" "8.59.3"
"@typescript-eslint/typescript-estree" "8.59.3"
"@typescript-eslint/utils" "8.59.3"
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/typescript-estree" "8.59.4"
"@typescript-eslint/utils" "8.59.4"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@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/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/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==
"@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==
dependencies:
"@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"
"@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"
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.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==
"@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==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.59.3"
"@typescript-eslint/types" "8.59.3"
"@typescript-eslint/typescript-estree" "8.59.3"
"@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.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==
"@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==
dependencies:
"@typescript-eslint/types" "8.59.3"
"@typescript-eslint/types" "8.59.4"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -5584,10 +5568,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.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==
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==
batch@0.6.1:
version "0.6.1"
@@ -7269,13 +7253,13 @@ encodeurl@~2.0.0:
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
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==
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==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.3.0"
tapable "^2.3.3"
entities@^2.0.0:
version "2.2.0"
@@ -7391,10 +7375,10 @@ es-iterator-helpers@^1.2.1:
iterator.prototype "^1.1.4"
safe-array-concat "^1.1.3"
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-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-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
@@ -9655,10 +9639,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.1:
version "4.3.1"
resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz"
integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==
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-utils@^1.2.3:
version "1.4.2"
@@ -14128,15 +14112,15 @@ synckit@^0.11.12:
dependencies:
"@pkgr/core" "^0.2.9"
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==
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==
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==
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==
dependencies:
"@jridgewell/trace-mapping" "^0.3.25"
jest-worker "^27.4.5"
@@ -14407,15 +14391,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
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==
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==
dependencies:
"@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-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@~6.0.3:
version "6.0.3"
@@ -14970,22 +14954,21 @@ webpack-merge@^6.0.1:
flat "^5.0.2"
wildcard "^2.0.1"
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-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-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.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==
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==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
"@types/json-schema" "^7.0.15"
"@webassemblyjs/ast" "^1.14.1"
@@ -14995,20 +14978,20 @@ webpack@^5.106.2, 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.20.0"
es-module-lexer "^2.0.0"
enhanced-resolve "^5.21.4"
es-module-lexer "^2.1.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.1"
loader-runner "^4.3.2"
mime-db "^1.54.0"
neo-async "^2.6.2"
schema-utils "^4.3.3"
tapable "^2.3.0"
terser-webpack-plugin "^5.3.17"
terser-webpack-plugin "^5.5.0"
watchpack "^2.5.1"
webpack-sources "^3.3.4"
webpack-sources "^3.4.1"
webpackbar@^7.0.0:
version "7.0.0"
@@ -92,6 +92,26 @@ 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
@@ -100,6 +120,7 @@ class Metric:
definition: str
description: str | None = None
aggregation: AggregationType | None = None
@dataclass(frozen=True)
@@ -17,12 +17,8 @@
* under the License.
*/
import { PropsWithChildren, ReactNode, SyntheticEvent } from 'react';
import {
ResizableBox,
ResizableBoxProps,
ResizeCallbackData,
} from 'react-resizable';
import { ReactNode, SyntheticEvent } from 'react';
import { ResizableBox, ResizeCallbackData } from 'react-resizable';
import { styled } from '@apache-superset/core/theme';
import 'react-resizable/css/styles.css';
@@ -46,14 +42,16 @@ export type Size = ResizeCallbackData['size'];
export default function ResizablePanel({
children,
heading = undefined,
heading,
initialSize = { width: 500, height: 300 },
minConstraints = [100, 100] as [number, number],
onResize,
...props
}: PropsWithChildren<Omit<ResizableBoxProps, 'width' | 'height'>> & {
}: {
children?: ReactNode;
heading?: ReactNode;
initialSize?: Size;
minConstraints?: [number, number];
onResize?: (e: SyntheticEvent, data: ResizeCallbackData) => void;
}) {
const { width, height } = initialSize;
return (
@@ -61,16 +59,14 @@ export default function ResizablePanel({
className="panel"
width={width}
height={height}
axis="both"
minConstraints={minConstraints}
onResize={
onResize
? (e: SyntheticEvent, data: ResizeCallbackData) => {
const { size } = data;
onResize(e, { ...data, size });
}
: undefined
}
{...props}
maxConstraints={[Infinity, Infinity]}
handleSize={[20, 20]}
lockAspectRatio={false}
resizeHandles={['se']}
transformScale={1}
onResize={onResize}
>
<>
{heading ? <div className="panel-heading">{heading}</div> : null}
+242 -247
View File
@@ -85,7 +85,7 @@
"antd": "^5.26.0",
"chrono-node": "^2.9.1",
"classnames": "^2.2.5",
"content-disposition": "^1.1.0",
"content-disposition": "^2.0.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.23.1",
"markdown-to-jsx": "^9.8.0",
"mapbox-gl": "^3.24.0",
"markdown-to-jsx": "^9.8.1",
"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.8.0",
"@types/node": "^25.9.1",
"@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": "^3.0.8",
"@types/react-resizable": "^4.0.0",
"@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.3",
"@typescript-eslint/parser": "^8.59.3",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"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.65.0",
"oxlint": "^1.66.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": "^3.1.3",
"react-resizable": "^4.0.1",
"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.9",
"ts-jest": "^29.4.10",
"tscw-config": "^1.1.2",
"tsx": "^4.22.0",
"tsx": "^4.22.3",
"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.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==",
"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==",
"cpu": [
"arm"
],
@@ -8663,9 +8663,9 @@
}
},
"node_modules/@oxlint/binding-android-arm64": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -8680,9 +8680,9 @@
}
},
"node_modules/@oxlint/binding-darwin-arm64": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -8697,9 +8697,9 @@
}
},
"node_modules/@oxlint/binding-darwin-x64": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -8714,9 +8714,9 @@
}
},
"node_modules/@oxlint/binding-freebsd-x64": {
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.65.0.tgz",
"integrity": "sha512-8lJQ7B6RloYDUhwVdbSpwT2eKsCN5KP1Scn18ly1tytCuhXhbs0nkfKHT4jWWZBJqmynWuzd+78bF7wILrj6pw==",
"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==",
"cpu": [
"x64"
],
@@ -8731,9 +8731,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
"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==",
"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==",
"cpu": [
"arm"
],
@@ -8748,9 +8748,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
"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==",
"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==",
"cpu": [
"arm"
],
@@ -8765,9 +8765,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-gnu": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -8782,9 +8782,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-musl": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -8799,9 +8799,9 @@
}
},
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
"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==",
"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==",
"cpu": [
"ppc64"
],
@@ -8816,9 +8816,9 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
"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==",
"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==",
"cpu": [
"riscv64"
],
@@ -8833,9 +8833,9 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-musl": {
"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==",
"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==",
"cpu": [
"riscv64"
],
@@ -8850,9 +8850,9 @@
}
},
"node_modules/@oxlint/binding-linux-s390x-gnu": {
"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==",
"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==",
"cpu": [
"s390x"
],
@@ -8867,9 +8867,9 @@
}
},
"node_modules/@oxlint/binding-linux-x64-gnu": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -8884,9 +8884,9 @@
}
},
"node_modules/@oxlint/binding-linux-x64-musl": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -8901,9 +8901,9 @@
}
},
"node_modules/@oxlint/binding-openharmony-arm64": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -8918,9 +8918,9 @@
}
},
"node_modules/@oxlint/binding-win32-arm64-msvc": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -8935,9 +8935,9 @@
}
},
"node_modules/@oxlint/binding-win32-ia32-msvc": {
"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==",
"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==",
"cpu": [
"ia32"
],
@@ -8952,9 +8952,9 @@
}
},
"node_modules/@oxlint/binding-win32-x64-msvc": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -13639,9 +13639,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"license": "MIT",
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
@@ -13796,12 +13796,14 @@
}
},
"node_modules/@types/react-resizable": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-3.0.8.tgz",
"integrity": "sha512-Pcvt2eGA7KNXldt1hkhVhAgZ8hK41m0mp89mFgQi7LAAEZiaLgm4fHJ5zbJZ/4m2LVaAyYrrRRv1LHDcrGQanA==",
"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.",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
"react-resizable": "*"
}
},
"node_modules/@types/react-router": {
@@ -14121,17 +14123,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@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",
"@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",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -14144,20 +14146,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.59.3",
"@typescript-eslint/parser": "^8.59.4",
"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.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"@typescript-eslint/tsconfig-utils": "^8.59.4",
"@typescript-eslint/types": "^8.59.4",
"debug": "^4.4.3"
},
"engines": {
@@ -14172,14 +14174,14 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14190,9 +14192,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14207,9 +14209,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14221,16 +14223,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@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",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -14249,16 +14251,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3"
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14273,13 +14275,13 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
"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==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/types": "8.59.4",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -14353,16 +14355,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz",
"integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@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",
"debug": "^4.4.3"
},
"engines": {
@@ -14378,14 +14380,14 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
"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==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"@typescript-eslint/tsconfig-utils": "^8.59.4",
"@typescript-eslint/types": "^8.59.4",
"debug": "^4.4.3"
},
"engines": {
@@ -14400,14 +14402,14 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14418,9 +14420,9 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14435,9 +14437,9 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14449,16 +14451,16 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@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",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -14477,13 +14479,13 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"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==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/types": "8.59.4",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -14618,15 +14620,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz",
"integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/utils": "8.59.4",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -14643,14 +14645,14 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": {
"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==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"@typescript-eslint/tsconfig-utils": "^8.59.4",
"@typescript-eslint/types": "^8.59.4",
"debug": "^4.4.3"
},
"engines": {
@@ -14665,14 +14667,14 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14683,9 +14685,9 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14700,9 +14702,9 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14714,16 +14716,16 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@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",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -14742,16 +14744,16 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3"
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14766,13 +14768,13 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
"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==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/types": "8.59.4",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -18958,9 +18960,9 @@
"license": "MIT"
},
"node_modules/content-disposition": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.0.tgz",
"integrity": "sha512-qqGFOrKmFP1lTfG24opOJFcTMza1BqyTSUKVbMGUP5uRsBH+C00Q1loOk+JSFshyRE0ji4HtCJeNN2WHWd6PGw==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -25494,12 +25496,6 @@
"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",
@@ -32882,13 +32878,13 @@
"license": "MIT"
},
"node_modules/mapbox-gl": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.23.1.tgz",
"integrity": "sha512-J5M32GunL5KcxvV3ZyLJdr7xtcQ3afAO3VjitLW0v2UVfHjXhq9NO4tkTpn4Dknqwq/pXZG7j1WBfmddLCA26w==",
"version": "3.24.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.24.0.tgz",
"integrity": "sha512-R+FdFUB3DnoE5FYASV7lGSiRyMkSblcZ2UEy7b2pt7s5ZbCxFIUPXd0E6iAFd8OdvdA2VtbvZZVylzAZNaurjA==",
"license": "SEE LICENSE IN LICENSE.txt",
"workspaces": [
"src/style-spec",
"packages/pmtiles-provider",
"plugins/mapbox-gl-pmtiles-provider",
"test/build/vite",
"test/build/webpack",
"test/build/typings"
@@ -32908,7 +32904,6 @@
"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",
@@ -33043,9 +33038,9 @@
}
},
"node_modules/markdown-to-jsx": {
"version": "9.8.0",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.8.0.tgz",
"integrity": "sha512-NWL0vDt6BeQgCxc2kK6g2SEPJpQuacu+NJk5j4QT4wlNmbsHdudoAbTGLCIyoPhfMiEqD1OZVBsYK0hvCky/HQ==",
"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==",
"license": "MIT",
"engines": {
"node": ">= 18"
@@ -37475,9 +37470,9 @@
}
},
"node_modules/oxlint": {
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.65.0.tgz",
"integrity": "sha512-ChUuE3Q7XnAbscvT4XLMsH7HFJmLgLVv9lu+RRgFL5wSXnDqUOzTp5IS8qWDBGd/ZDSzQ2tbX8fjAmijlGLC7A==",
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.66.0.tgz",
"integrity": "sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -37490,25 +37485,25 @@
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@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"
"@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"
},
"peerDependencies": {
"oxlint-tsgolint": ">=0.22.1"
@@ -40811,9 +40806,9 @@
}
},
"node_modules/react-resizable": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz",
"integrity": "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-4.0.1.tgz",
"integrity": "sha512-FR25Rcfxpi1iKiC7taIrqc1Tz6VnslqM94/IrA1LxoM5C3ap2EqaKLnCit/aKrcn3H4wfzO0nFBadFBc+SzEWA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -42585,9 +42580,9 @@
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -45228,9 +45223,9 @@
}
},
"node_modules/ts-jest": {
"version": "29.4.9",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz",
"integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==",
"version": "29.4.10",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.10.tgz",
"integrity": "sha512-vMTlTTtvz5aKZgzOoc7DQ5TzAL2fCzl8JnG1+ZpwjQa/g0xLlwE44yQ+1Cao9ZP1xVv9y5g34IFXEiqGOGFBUA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -45240,7 +45235,7 @@
"json5": "^2.2.3",
"lodash.memoize": "^4.1.2",
"make-error": "^1.3.6",
"semver": "^7.7.4",
"semver": "^7.8.0",
"type-fest": "^4.41.0",
"yargs-parser": "^21.1.1"
},
@@ -45441,9 +45436,9 @@
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz",
"integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==",
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz",
"integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -48915,7 +48910,7 @@
"dependencies": {
"chalk": "^5.6.2",
"lodash-es": "^4.18.1",
"yeoman-generator": "^8.1.2",
"yeoman-generator": "^8.2.2",
"yosay": "^3.0.0"
},
"devDependencies": {
@@ -49388,7 +49383,7 @@
"react-js-cron": "^5.2.0",
"react-markdown": "^8.0.7",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^16.1.1",
"react-syntax-highlighter": "^16.1.0",
"react-ultimate-pagination": "^1.3.2",
"regenerator-runtime": "^0.14.1",
"rehype-raw": "^7.0.0",
@@ -49408,7 +49403,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.0",
"@types/lodash": "^4.17.24",
"@types/node": "^25.8.0",
"@types/node": "^25.9.1",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -49988,7 +49983,7 @@
"acorn": "^8.16.0",
"d3-array": "^3.2.4",
"lodash": "^4.18.1",
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
@@ -50098,9 +50093,9 @@
"license": "Apache-2.0",
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.23.1",
"mapbox-gl": "^3.24.0",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.0",
"react-map-gl": "^8.1.1",
"supercluster": "^8.0.1"
},
"peerDependencies": {
+11 -11
View File
@@ -166,7 +166,7 @@
"antd": "^5.26.0",
"chrono-node": "^2.9.1",
"classnames": "^2.2.5",
"content-disposition": "^1.1.0",
"content-disposition": "^2.0.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.23.1",
"markdown-to-jsx": "^9.8.0",
"mapbox-gl": "^3.24.0",
"markdown-to-jsx": "^9.8.1",
"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.8.0",
"@types/node": "^25.9.1",
"@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": "^3.0.8",
"@types/react-resizable": "^4.0.0",
"@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.3",
"@typescript-eslint/parser": "^8.59.3",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"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.65.0",
"oxlint": "^1.66.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": "^3.1.3",
"react-resizable": "^4.0.1",
"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.9",
"ts-jest": "^29.4.10",
"tscw-config": "^1.1.2",
"tsx": "^4.22.0",
"tsx": "^4.22.3",
"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.1.2",
"yeoman-generator": "^8.2.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.1",
"react-syntax-highlighter": "^16.1.0",
"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.8.0",
"@types/node": "^25.9.1",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -63,7 +63,8 @@ export default async function parseResponse<T extends ParseMethod = 'json'>(
(value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
value?.isLessThan?.(Number.MIN_SAFE_INTEGER))
) {
return BigInt(value);
// toFixed() avoids scientific notation, which BigInt() rejects.
return BigInt(value.toFixed());
}
// // `json-bigint` could not handle floats well, see sidorares/json-bigint#62
// // TODO: clean up after json-bigint>1.0.1 is released
@@ -183,6 +183,26 @@ 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,8 +95,11 @@ class FakeMessageChannel {
const port2 = new FakeMessagePort();
port1.otherPort = port2;
port2.otherPort = port1;
this.port1 = port1;
this.port2 = port2;
// 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;
}
}
@@ -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: boolean;
debugMode = false;
private isInitialised: boolean;
private isInitialised = false;
constructor(params?: Params) {
if (!params) {
@@ -56,6 +56,7 @@ import {
VisualMapComponent,
LegendComponent,
DataZoomComponent,
type DataZoomComponentOption,
ToolboxComponent,
GraphicComponent,
AriaComponent,
@@ -280,12 +281,56 @@ 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]);
@@ -0,0 +1,192 @@
/**
* 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.23.1",
"mapbox-gl": "^3.24.0",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.0",
"react-map-gl": "^8.1.1",
"supercluster": "^8.0.1"
},
"peerDependencies": {
@@ -97,7 +97,7 @@ export function createWrapper(options?: Options) {
}
if (useDnd) {
// @ts-expect-error react-dnd types not updated for React 18
// @ts-ignore react-dnd's DndProviderProps omits `children` under React 18 types
result = <DndProvider backend={HTML5Backend}>{result}</DndProvider>;
}
+12 -6
View File
@@ -1671,7 +1671,7 @@ export interface VizOptions {
export function createDatasource(
vizOptions: VizOptions,
): SqlLabThunkAction<Promise<unknown>> {
): SqlLabThunkAction<Promise<{ id: number }>> {
return (dispatch: AppDispatch) => {
dispatch(createDatasourceStarted());
const { dbId, catalog, schema, datasourceName, sql, templateParams } =
@@ -1691,9 +1691,10 @@ export function createDatasource(
}),
})
.then(({ json }) => {
dispatch(createDatasourceSuccess(json as { id: number }));
const result = json as { id: number };
dispatch(createDatasourceSuccess(result));
return Promise.resolve(json);
return result;
})
.catch(error => {
getClientErrorObject(error).then(e => {
@@ -1712,7 +1713,7 @@ export function createDatasource(
export function createCtasDatasource(
vizOptions: Record<string, unknown>,
): SqlLabThunkAction<Promise<{ id: number }>> {
): SqlLabThunkAction<Promise<{ table_id: number }>> {
return (dispatch: AppDispatch) => {
dispatch(createDatasourceStarted());
return SupersetClient.post({
@@ -1720,9 +1721,14 @@ export function createCtasDatasource(
jsonPayload: vizOptions,
})
.then(({ json }) => {
dispatch(createDatasourceSuccess(json.result));
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 }));
return json.result;
return result;
})
.catch(() => {
const errorMsg = t('An error occurred while creating the data source');
@@ -19,7 +19,8 @@
import { useRef, useEffect, FC, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { logging } from '@apache-superset/core/utils';
import {
SqlLabRootState,
@@ -86,7 +87,7 @@ const EditorAutoSync: FC = () => {
const editorTabLastUpdatedAt = useSelector<SqlLabRootState, number>(
state => state.sqlLab.editorTabLastUpdatedAt,
);
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const lastSavedTimestampRef = useRef<number>(editorTabLastUpdatedAt);
const currentQueryEditorId = useSelector<SqlLabRootState, string>(
@@ -17,7 +17,8 @@
* under the License.
*/
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { usePrevious } from '@superset-ui/core';
import { css, useTheme } from '@apache-superset/core/theme';
import { Global } from '@emotion/react';
@@ -136,7 +137,7 @@ const EditorWrapper = ({
height,
hotkeys,
}: EditorWrapperProps) => {
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const queryEditor = useQueryEditor(queryEditorId, [
'id',
'dbId',
@@ -17,7 +17,8 @@
* under the License.
*/
import { useEffect, useMemo, useRef } from 'react';
import { useDispatch, useStore } from 'react-redux';
import { useStore } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { t } from '@apache-superset/core/translation';
import { getExtensionsRegistry } from '@superset-ui/core';
@@ -68,7 +69,7 @@ export function useKeywords(
catalog,
schema,
});
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const hasFetchedKeywords = useRef(false);
// skipFetch is used to prevent re-evaluating memoized keywords
// due to updated api results by skip flag
@@ -16,9 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useSelector, useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { t } from '@apache-superset/core/translation';
import { JsonObject, VizType } from '@superset-ui/core';
import { VizType } from '@superset-ui/core';
import {
createCtasDatasource,
addInfoToast,
@@ -45,7 +46,7 @@ const ExploreCtasResultsButton = ({
const errorMessage = useSelector(
(state: SqlLabRootState) => state.sqlLab.errorMessage,
);
const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
const dispatch = useAppDispatch();
const buildVizOptions = {
table_name: table,
@@ -56,7 +57,7 @@ const ExploreCtasResultsButton = ({
const visualize = () => {
dispatch(createCtasDatasource(buildVizOptions))
.then((data: { table_id: number }) => {
.then(data => {
const formData = {
datasource: `${data.table_id}__table`,
metrics: ['count'],
@@ -17,7 +17,8 @@
* under the License.
*/
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import URI from 'urijs';
import { pick } from 'lodash';
import { useComponentDidUpdate } from '@superset-ui/core';
@@ -49,7 +50,7 @@ const PopEditorTab: React.FC<{ children?: React.ReactNode }> = ({
({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0],
);
const [updatedUrl, setUpdatedUrl] = useState<string>(SQL_LAB_URL);
const dispatch = useDispatch();
const dispatch = useAppDispatch();
useComponentDidUpdate(() => {
setQueryEditorId(assigned => assigned ?? activeQueryEditorId);
if (activeQueryEditorId) {
@@ -17,7 +17,8 @@
* under the License.
*/
import { useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { isObject } from 'lodash';
import rison from 'rison';
import {
@@ -82,7 +83,7 @@ function QueryAutoRefresh({
.map(({ id }) => id),
),
);
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const checkForRefresh = () => {
const shouldRequestChecking = shouldCheckForQueries(queries);
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useDispatch } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
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 = useDispatch();
const dispatch = useAppDispatch();
const queryEditor = useQueryEditor(queryEditorId, ['id', 'queryLimit']);
const queryLimit = queryEditor.queryLimit || defaultQueryLimit;
@@ -30,7 +30,8 @@ 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, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import {
queryEditorSetSql,
@@ -92,7 +93,7 @@ const QueryTable = ({
latestQueryId,
}: QueryTableProps) => {
const theme = useTheme();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const [selectedQuery, setSelectedQuery] = useState<QueryResponse | null>(
null,
);
@@ -27,7 +27,8 @@ import {
} from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useHistory } from 'react-router-dom';
import { pick } from 'lodash';
import {
@@ -231,7 +232,7 @@ const ResultSet = ({
canCopyClipboardSqlLab: canCopyClipboard,
} = usePermissions();
const history = useHistory();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const logAction = useLogAction({ queryId, sqlEditorId: query.sqlEditorId });
const { showConfirm, ConfirmModal } = useConfirmModal();
@@ -16,8 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import * as reactRedux from 'react-redux';
import { act } from 'react';
import { act, type ComponentProps } from 'react';
import {
cleanup,
fireEvent,
@@ -40,6 +39,19 @@ 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,
@@ -47,17 +59,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 the createDatasource action
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
// 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.
jest.mock('src/SqlLab/actions/sqlLab', () => ({
createDatasource: jest.fn(),
createDatasource: jest.fn(() => () => Promise.resolve({ id: 123 })),
}));
jest.mock('src/explore/exploreUtils/formData', () => ({
postFormData: jest.fn(),
@@ -70,7 +82,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', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
const saveRadioBtn = screen.getByRole('radio', {
name: /save as new/i,
@@ -87,7 +99,7 @@ describe('SaveDatasetModal', () => {
});
test('renders an "Overwrite existing" field', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
const overwriteRadioBtn = screen.getByRole('radio', {
name: /overwrite existing/i,
@@ -103,20 +115,20 @@ describe('SaveDatasetModal', () => {
});
test('renders a close button', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
});
test('renders a save button when "Save as new" is selected', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
// "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', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
// Click the overwrite radio button to reveal the overwrite confirmation and back buttons
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -130,8 +142,7 @@ describe('SaveDatasetModal', () => {
});
test('renders the overwrite button as disabled until an existing dataset is selected', async () => {
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
// Click the overwrite radio button
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -168,8 +179,7 @@ describe('SaveDatasetModal', () => {
});
test('renders a confirm overwrite screen when overwrite is clicked', async () => {
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
// Click the overwrite radio button
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -215,11 +225,7 @@ describe('SaveDatasetModal', () => {
});
test('sends the schema when creating the dataset', async () => {
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -240,17 +246,9 @@ describe('SaveDatasetModal', () => {
});
test('sends the catalog when creating the dataset', async () => {
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
render(
<SaveDatasetModal
{...mockedProps}
datasource={{ ...mockedProps.datasource, catalog: 'public' }}
/>,
{ useRedux: true },
);
renderModal({
datasource: { ...mockedProps.datasource, catalog: 'public' },
});
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -271,7 +269,7 @@ describe('SaveDatasetModal', () => {
});
test('does not renders a checkbox button when template processing is disabled', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
});
@@ -280,7 +278,7 @@ describe('SaveDatasetModal', () => {
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
};
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
expect(screen.getByRole('checkbox')).toBeInTheDocument();
});
@@ -289,15 +287,11 @@ describe('SaveDatasetModal', () => {
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
};
const propsWithTemplateParam = {
...mockedProps,
renderModal({
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' } });
@@ -324,15 +318,11 @@ describe('SaveDatasetModal', () => {
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
};
const propsWithTemplateParam = {
...mockedProps,
renderModal({
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' } });
@@ -393,19 +383,11 @@ describe('SaveDatasetModal', () => {
.spyOn(SupersetClient, 'put')
.mockResolvedValue({ json: { result: { id: 0 } } } as any);
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
const propsWithTemplateParam = {
...mockedProps,
renderModal({
datasource: {
...testQuery,
templateParams: JSON.stringify({ my_param: 12, _filters: 'foo' }),
},
};
render(<SaveDatasetModal {...propsWithTemplateParam} />, {
useRedux: true,
});
// Check the "Include Template Parameters" checkbox
@@ -443,19 +425,11 @@ describe('SaveDatasetModal', () => {
.spyOn(SupersetClient, 'put')
.mockResolvedValue({ json: { result: { id: 0 } } } as any);
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
const propsWithTemplateParam = {
...mockedProps,
renderModal({
datasource: {
...testQuery,
templateParams: JSON.stringify({ my_param: 12 }),
},
};
render(<SaveDatasetModal {...propsWithTemplateParam} />, {
useRedux: true,
});
// Do NOT check the "Include Template Parameters" checkbox
@@ -489,12 +463,9 @@ describe('SaveDatasetModal', () => {
'postFormData',
);
const dummyDispatch = jest.fn().mockResolvedValue({ id: 123 });
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
postFormData.mockResolvedValue('chart_key_123');
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -34,7 +34,6 @@ import { t } from '@apache-superset/core/translation';
import {
SupersetClient,
JsonResponse,
JsonObject,
QueryResponse,
QueryFormData,
VizType,
@@ -44,16 +43,14 @@ import {
} from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import { useSelector, useDispatch } from 'react-redux';
import { useAppDispatch, useAppSelector } from 'src/views/store';
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';
@@ -221,7 +218,7 @@ export const SaveDatasetModal = ({
openWindow = true,
formData = {},
}: SaveDatasetModalProps) => {
const defaultVizType = useSelector<SqlLabRootState, string>(
const defaultVizType = useAppSelector(
state => state.common?.conf?.DEFAULT_VIZ_TYPE || VizType.Table,
);
@@ -240,8 +237,8 @@ export const SaveDatasetModal = ({
>(undefined);
const [loading, setLoading] = useState<boolean>(false);
const user = useSelector<SqlLabRootState, User>(state => state.user);
const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
const user = useAppSelector(state => state.user);
const dispatch = useAppDispatch();
const [includeTemplateParameters, setIncludeTemplateParameters] =
useState(false);
@@ -17,7 +17,8 @@
* under the License.
*/
import { createRef, useCallback, useMemo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { nanoid } from 'nanoid';
import Tabs from '@superset-ui/core/components/Tabs';
import { t } from '@apache-superset/core/translation';
@@ -105,7 +106,7 @@ const SouthPane = ({
const { id, tabViewId } = useQueryEditor(queryEditorId, ['tabViewId']);
const editorId = tabViewId ?? id;
const theme = useTheme();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const viewItems = views.getViews(ViewLocations.sqllab.panels) || [];
const { offline, tables } = useSelector(
({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
@@ -30,7 +30,8 @@ import {
import type { editors } from '@apache-superset/core';
import useEffectEvent from 'src/hooks/useEffectEvent';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import AutoSizer from 'react-virtualized-auto-sizer';
import { t } from '@apache-superset/core/translation';
import {
@@ -237,7 +238,7 @@ const SqlEditor: FC<Props> = ({
scheduleQueryWarning,
}) => {
const theme = useTheme();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const { database, latestQuery, currentQueryEditorId, hasSqlStatement } =
useSelector<
@@ -17,7 +17,7 @@
* under the License.
*/
import { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
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 = useDispatch();
const dispatch = useAppDispatch();
const shouldShowReset = window.location.search === '?reset=1';
// Modal state for Database/Catalog/Schema selector
@@ -19,7 +19,8 @@
import { useMemo, FC } from 'react';
import { bindActionCreators } from 'redux';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { useSelector, shallowEqual } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { MenuDotsDropdown } from '@superset-ui/core/components';
import { Menu, MenuItemType } from '@superset-ui/core/components/Menu';
import { t } from '@apache-superset/core/translation';
@@ -90,7 +91,7 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
);
const StatusIcon = queryState ? STATE_ICONS[queryState] : STATE_ICONS.running;
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const actions = useMemo(
() =>
bindActionCreators(
@@ -17,7 +17,8 @@
* under the License.
*/
import { useEffect, useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { SqlLabRootState } from 'src/SqlLab/types';
import {
@@ -41,7 +42,7 @@ export default function useDatabaseSelector(queryEditorId: string) {
SqlLabRootState,
SqlLabRootState['sqlLab']['databases']
>(({ sqlLab }) => sqlLab.databases);
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const queryEditor = useQueryEditor(queryEditorId, [
'dbId',
'catalog',
@@ -17,7 +17,8 @@
* under the License.
*/
import { useState, useRef, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import type { QueryEditor, SqlLabRootState, Table } from 'src/SqlLab/types';
import {
ButtonGroup,
@@ -75,7 +76,7 @@ const Fade = styled.div`
const TableElement = ({ table, ...props }: TableElementProps) => {
const { dbId, catalog, schema, name, expanded, id } = table;
const theme = useTheme();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const {
currentData: tableMetadata,
isSuccess: isMetadataSuccess,
@@ -25,7 +25,8 @@ import {
type ChangeEvent,
useMemo,
} from 'react';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { useSelector, shallowEqual } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { styled, css, useTheme } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -163,7 +164,7 @@ const savePinnedSchemasToStorage = (
};
const TableExploreTree: React.FC<Props> = ({ queryEditorId }) => {
const dispatch = useDispatch();
const dispatch = useAppDispatch();
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 { useDispatch } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { t } from '@apache-superset/core/translation';
import {
Table,
@@ -130,7 +130,7 @@ const useTreeData = ({
catalog,
pinnedTables,
}: UseTreeDataParams): UseTreeDataResult => {
const reduxDispatch = useDispatch();
const reduxDispatch = useAppDispatch();
// Schema data from API
const {
currentData: schemaData,
@@ -17,7 +17,8 @@
* under the License.
*/
import { type FC, useCallback, useMemo, useRef, useState } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { nanoid } from 'nanoid';
import { t } from '@apache-superset/core/translation';
import { ClientErrorObject, getExtensionsRegistry } from '@superset-ui/core';
@@ -110,7 +111,7 @@ const renderWell = (partitions: TableMetaData['partitions']) => {
};
const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const theme = useTheme();
const [databaseName, backend, disableDataPreview] = useSelector<
SqlLabRootState,
@@ -28,7 +28,7 @@ import fetchMock from 'fetch-mock';
import { SupersetClient } from '@superset-ui/core';
import mockDatasource from 'spec/fixtures/mockDatasource';
import React from 'react';
import DatasourceModalComponent from '.';
import DatasourceModalComponent, { buildExtraJsonObject } from '.';
// Cast to accept partial mock props in tests
const DatasourceModal = DatasourceModalComponent as unknown as React.FC<
@@ -309,3 +309,35 @@ 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)`
}
`;
function buildExtraJsonObject(
export function buildExtraJsonObject(
item: DatasetObject['metrics'][0] | DatasetObject['columns'][0],
) {
const certification =
@@ -83,7 +83,7 @@ function buildExtraJsonObject(
: undefined;
return JSON.stringify({
certification,
warning_markdown: item?.warning_markdown,
warning_markdown: item?.warning_markdown || undefined,
});
}
@@ -0,0 +1,267 @@
/**
* 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,14 +58,22 @@ function SelectFilter(
) {
const [selectedOption, setSelectedOption] = useState(initialValue);
const onChange = (selected: SelectOption) => {
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.
onSelect(
selected
? {
label:
typeof selected.label === 'string'
? selected.label
: String(selected.value),
: (option?.title ?? String(selected.value)),
value: selected.value,
}
: undefined,
@@ -26,6 +26,10 @@ 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;
}
@@ -90,7 +90,13 @@ const defaultState = {
superset_can_explore: false,
superset_can_share: false,
superset_can_csv: false,
common: { conf: { SUPERSET_WEBSERVER_TIMEOUT: 0, SQL_MAX_ROW: 666 } },
common: {
conf: {
SUPERSET_WEBSERVER_TIMEOUT: 0,
SQL_MAX_ROW: 666,
TABLE_VIZ_MAX_ROW_SERVER: 999,
},
},
},
dashboardLayout: {
present: {},
@@ -201,7 +207,7 @@ test('should call exportChart when exportCSV is clicked', async () => {
stubbedExportCSV.mockRestore();
});
test('should call exportChart with row_limit props.maxRows when exportFullCSV is clicked', async () => {
test('should call exportChart with row_limit TABLE_VIZ_MAX_ROW_SERVER when exportFullCSV is clicked', async () => {
(global as any).featureFlags = {
[FeatureFlag.AllowFullCsvExport]: true,
};
@@ -222,7 +228,8 @@ test('should call exportChart with row_limit props.maxRows when exportFullCSV is
expect(stubbedExportCSV).toHaveBeenCalledWith(
expect.objectContaining({
formData: expect.objectContaining({
row_limit: 666,
row_limit: 999,
full_export: true,
dashboardId: 111,
}),
resultType: 'full',
@@ -256,7 +263,7 @@ test('should call exportChart when exportXLSX is clicked', async () => {
stubbedExportXLSX.mockRestore();
});
test('should call exportChart with row_limit props.maxRows when exportFullXLSX is clicked', async () => {
test('should call exportChart with row_limit TABLE_VIZ_MAX_ROW_SERVER when exportFullXLSX is clicked', async () => {
(global as any).featureFlags = {
[FeatureFlag.AllowFullCsvExport]: true,
};
@@ -277,7 +284,8 @@ test('should call exportChart with row_limit props.maxRows when exportFullXLSX i
expect(stubbedExportXLSX).toHaveBeenCalledWith(
expect.objectContaining({
formData: expect.objectContaining({
row_limit: 666,
row_limit: 999,
full_export: true,
dashboardId: 111,
}),
resultType: 'full',
@@ -224,8 +224,10 @@ const Chart = (props: ChartProps) => {
const emitCrossFilters = useSelector(
(state: RootState) => !!state.dashboardInfo.crossFiltersEnabled,
);
const maxRows: number = useSelector(
(state: RootState) => state.dashboardInfo.common.conf.SQL_MAX_ROW as number,
const fullExportMaxRows: number = useSelector(
(state: RootState) =>
(state.dashboardInfo.common.conf.TABLE_VIZ_MAX_ROW_SERVER as number) ||
(state.dashboardInfo.common.conf.SQL_MAX_ROW as number),
);
const streamingThreshold: number = useSelector(
(state: RootState) =>
@@ -480,7 +482,7 @@ const Chart = (props: ChartProps) => {
(formData as JsonObject).dashboardId = dashboardInfo.id;
const exportTable = useCallback(
(format: string, isFullCSV: boolean, isPivot = false) => {
(format: string, isFullExport: boolean, isPivot = false) => {
const logAction =
format === 'csv'
? LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART
@@ -490,8 +492,11 @@ const Chart = (props: ChartProps) => {
is_cached: isCached,
});
const exportFormData = isFullCSV
? { ...formData, row_limit: maxRows }
// For a "full" export, raise the requested row_limit and flag the
// request with full_export so the backend lifts the row-limit cap to
// TABLE_VIZ_MAX_ROW_SERVER (gated by the ALLOW_FULL_CSV_EXPORT flag).
const exportFormData = isFullExport
? { ...formData, row_limit: fullExportMaxRows, full_export: true }
: formData;
const resultType = isPivot ? 'post_processed' : 'full';
@@ -579,7 +584,7 @@ const Chart = (props: ChartProps) => {
sliceVizType,
isCached,
formData,
maxRows,
fullExportMaxRows,
dataMaskOwnState,
chartState,
props.id,
@@ -25,7 +25,7 @@ import {
SupersetClient,
} from '@superset-ui/core';
import { MenuItem } from '@superset-ui/core/components/Menu';
import contentDisposition from 'content-disposition';
import { parse as parseContentDisposition } 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 = contentDisposition.parse(disposition);
const parsed = parseContentDisposition(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 contentDisposition from 'content-disposition';
import { parse as parseContentDisposition } 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 = contentDisposition.parse(disposition);
const parsed = parseContentDisposition(disposition);
if (parsed?.parameters?.filename) {
fileName = parsed.parameters.filename;
}
@@ -103,10 +103,12 @@ 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(
REPORT_ENDPOINT,
'glob:*/api/v1/report/subscribe',
{ id: 1, result: {} },
{ name: 'post-report' },
{ name: 'post-subscribe' },
);
render(<ReportModal {...defaultProps} />, { useRedux: true });
@@ -114,22 +116,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 from the modal submit path
// Verify exactly one POST to the subscribe endpoint
await waitFor(() => {
const postCalls = fetchMock.callHistory.calls('post-report');
const postCalls = fetchMock.callHistory.calls('post-subscribe');
expect(postCalls).toHaveLength(1);
});
const postCalls = fetchMock.callHistory.calls('post-report');
const postCalls = fetchMock.callHistory.calls('post-subscribe');
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();
expect(body.recipients).toBeDefined();
expect(body.recipients[0].type).toBe('Email');
// creation_method, owners, and recipients are set server-side; not in the client payload
expect(body.creation_method).toBeUndefined();
expect(body.recipients).toBeUndefined();
fetchMock.removeRoute('post-report');
fetchMock.removeRoute('post-subscribe');
});
test('text-based chart hides screenshot width and shows message content', () => {
@@ -174,6 +174,28 @@ 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 {
@@ -255,5 +277,6 @@ 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,26 +179,13 @@ function ReportModal({
}, [isEditMode, report]);
const onSave = async () => {
// Create new Report
const newReportValues: Partial<ReportObject> = {
const commonFields: 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,
@@ -209,12 +196,27 @@ 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, newReportValues as ReportObject),
editReport(currentReport.id, {
...commonFields,
creation_method: creationMethod,
owners: [userId],
recipients: [
{
recipient_config_json: {
target: userEmail,
ccTarget: ccEmail,
bccTarget: bccEmail,
},
type: 'Email',
},
],
} as ReportObject),
);
} else {
// Create new report (either not in edit mode, or edit mode without valid ID)
await dispatch(addReport(newReportValues as ReportObject));
// Subscribe path: creation_method, owners, and recipients are set server-side.
await dispatch(subscribeReport(commonFields as ReportObject));
}
onHide();
} catch (e) {
@@ -21,11 +21,13 @@ import { omit } from 'lodash';
import {
SET_REPORT,
ADD_REPORT,
SUBSCRIBE_REPORT,
EDIT_REPORT,
DELETE_REPORT,
ReportAction,
SetReportAction,
AddReportAction,
SubscribeReportAction,
EditReportAction,
DeleteReportAction,
} from './actions';
@@ -105,6 +107,25 @@ 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,6 +117,7 @@ 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
@@ -131,7 +132,11 @@ const setupLaunchQueue = (fileHandle: MockFileHandle | null = null) => {
if (fileHandle) {
const id = setTimeout(() => {
pendingTimerIds.delete(id);
consumer({ files: [fileHandle] });
consumerPromises.push(
Promise.resolve(consumer({ files: [fileHandle] })).then(
() => undefined,
),
);
}, 0);
pendingTimerIds.add(id);
}
@@ -165,9 +170,19 @@ beforeEach(() => {
.launchQueue;
});
afterEach(() => {
afterEach(async () => {
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;
});
+8 -5
View File
@@ -18,7 +18,7 @@
*/
import { SupersetClient } from '@superset-ui/core';
import { logging } from '@apache-superset/core/utils';
import contentDisposition from 'content-disposition';
import { parse as parseContentDisposition } from 'content-disposition';
import handleResourceExport from './export';
// Mock dependencies
@@ -35,7 +35,10 @@ jest.mock('@apache-superset/core/utils', () => ({
},
}));
jest.mock('content-disposition');
jest.mock('content-disposition', () => ({
parse: jest.fn(),
__esModule: true,
}));
// Default no-op mock for pathUtils; specific tests customize ensureAppRoot to simulate app root prefixing
jest.mock('./pathUtils', () => ({
@@ -156,7 +159,7 @@ test('uses default filename when Content-Disposition is missing', async () => {
});
test('handles Content-Disposition parsing errors gracefully', async () => {
(contentDisposition.parse as jest.Mock).mockImplementationOnce(() => {
(parseContentDisposition as jest.Mock).mockImplementationOnce(() => {
throw new Error('Invalid header');
});
@@ -208,7 +211,7 @@ test('exports multiple resources with correct IDs', async () => {
});
test('parses filename from Content-Disposition with quotes', async () => {
(contentDisposition.parse as jest.Mock).mockReturnValueOnce({
(parseContentDisposition as jest.Mock).mockReturnValueOnce({
type: 'attachment',
parameters: { filename: 'my_custom_export.zip' },
});
@@ -360,7 +363,7 @@ test('handles malformed Content-Disposition header', async () => {
} as unknown as Response;
(SupersetClient.get as jest.Mock).mockResolvedValue(mockResponse);
(contentDisposition.parse as jest.Mock).mockImplementationOnce(() => {
(parseContentDisposition as jest.Mock).mockImplementationOnce(() => {
throw new Error('Parse error');
});
+2 -2
View File
@@ -19,7 +19,7 @@
import { SupersetClient } from '@superset-ui/core';
import { logging } from '@apache-superset/core/utils';
import rison from 'rison';
import contentDisposition from 'content-disposition';
import { parse as parseContentDisposition } 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 = contentDisposition.parse(disposition);
const parsed = parseContentDisposition(disposition);
if (parsed?.parameters?.filename) {
fileName = parsed.parameters.filename;
}
+11 -2
View File
@@ -22,12 +22,13 @@ import {
createListenerMiddleware,
StoreEnhancer,
} from '@reduxjs/toolkit';
import type { AnyAction } from 'redux';
import {
useDispatch,
useSelector,
type TypedUseSelectorHook,
} from 'react-redux';
import thunk from 'redux-thunk';
import thunk, { type ThunkDispatch } from 'redux-thunk';
import { api } from 'src/hooks/apiResources/queryApi';
import messageToastReducer from 'src/components/MessageToasts/reducers';
import charts from 'src/components/Chart/chartReducer';
@@ -188,6 +189,14 @@ 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.
export type AppDispatch = typeof store.dispatch;
//
// 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 const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
+146 -146
View File
@@ -23,21 +23,21 @@
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash": "^4.17.24",
"@types/node": "^25.8.0",
"@types/node": "^25.9.1",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"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.9",
"ts-jest": "^29.4.10",
"ts-node": "^10.9.2",
"tscw-config": "^1.1.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.59.3"
"typescript-eslint": "^8.59.4"
},
"engines": {
"node": "^22.22.0",
@@ -1798,9 +1798,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1844,17 +1844,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@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",
"@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",
"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.3",
"@typescript-eslint/parser": "^8.59.4",
"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.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz",
"integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@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",
"debug": "^4.4.3"
},
"engines": {
@@ -1908,14 +1908,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"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==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"@typescript-eslint/tsconfig-utils": "^8.59.4",
"@typescript-eslint/types": "^8.59.4",
"debug": "^4.4.3"
},
"engines": {
@@ -1930,14 +1930,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1948,9 +1948,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1965,15 +1965,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz",
"integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/utils": "8.59.4",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1990,9 +1990,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2004,16 +2004,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@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",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -2071,16 +2071,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3"
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2095,13 +2095,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"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==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/types": "8.59.4",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -5695,9 +5695,9 @@
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -6016,9 +6016,9 @@
}
},
"node_modules/ts-jest": {
"version": "29.4.9",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz",
"integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==",
"version": "29.4.10",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.10.tgz",
"integrity": "sha512-vMTlTTtvz5aKZgzOoc7DQ5TzAL2fCzl8JnG1+ZpwjQa/g0xLlwE44yQ+1Cao9ZP1xVv9y5g34IFXEiqGOGFBUA==",
"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.7.4",
"semver": "^7.8.0",
"type-fest": "^4.41.0",
"yargs-parser": "^21.1.1"
},
@@ -6199,16 +6199,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
"integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz",
"integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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-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"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7894,9 +7894,9 @@
"dev": true
},
"@types/node": {
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"dev": true,
"requires": {
"undici-types": ">=7.24.0 <7.24.7"
@@ -7938,16 +7938,16 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
"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==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.12.2",
"@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",
"@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",
"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.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz",
"integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==",
"dev": true,
"requires": {
"@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",
"@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",
"debug": "^4.4.3"
}
},
"@typescript-eslint/project-service": {
"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==",
"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==",
"dev": true,
"requires": {
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"@typescript-eslint/tsconfig-utils": "^8.59.4",
"@typescript-eslint/types": "^8.59.4",
"debug": "^4.4.3"
}
},
"@typescript-eslint/scope-manager": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/visitor-keys": "8.59.4"
}
},
"@typescript-eslint/tsconfig-utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"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==",
"dev": true,
"requires": {}
},
"@typescript-eslint/type-utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz",
"integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4",
"@typescript-eslint/utils": "8.59.4",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
}
},
"@typescript-eslint/types": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"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==",
"dev": true,
"requires": {
"@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",
"@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",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -8065,24 +8065,24 @@
}
},
"@typescript-eslint/utils": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"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==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3"
"@typescript-eslint/scope-manager": "8.59.4",
"@typescript-eslint/types": "8.59.4",
"@typescript-eslint/typescript-estree": "8.59.4"
}
},
"@typescript-eslint/visitor-keys": {
"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==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/types": "8.59.4",
"eslint-visitor-keys": "^5.0.0"
},
"dependencies": {
@@ -10730,9 +10730,9 @@
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
},
"semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="
},
"shebang-command": {
"version": "2.0.0",
@@ -10954,9 +10954,9 @@
"requires": {}
},
"ts-jest": {
"version": "29.4.9",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz",
"integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==",
"version": "29.4.10",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.10.tgz",
"integrity": "sha512-vMTlTTtvz5aKZgzOoc7DQ5TzAL2fCzl8JnG1+ZpwjQa/g0xLlwE44yQ+1Cao9ZP1xVv9y5g34IFXEiqGOGFBUA==",
"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.7.4",
"semver": "^7.8.0",
"type-fest": "^4.41.0",
"yargs-parser": "^21.1.1"
}
@@ -11044,15 +11044,15 @@
"dev": true
},
"typescript-eslint": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
"integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
"version": "8.59.4",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz",
"integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==",
"dev": true,
"requires": {
"@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-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"
}
},
"uglify-js": {
+5 -5
View File
@@ -31,21 +31,21 @@
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash": "^4.17.24",
"@types/node": "^25.8.0",
"@types/node": "^25.9.1",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"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.9",
"ts-jest": "^29.4.10",
"ts-node": "^10.9.2",
"tscw-config": "^1.1.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.59.3"
"typescript-eslint": "^8.59.4"
},
"engines": {
"node": "^22.22.0",
@@ -66,7 +66,22 @@ class StreamingCSVExportCommand(BaseStreamingCSVExportCommand):
# Note: datasource should already be attached to a session from query_context
datasource = self._query_context.datasource
query_obj = self._query_context.queries[0]
sql_query = datasource.get_query_str(query_obj.to_dict())
query_dict = query_obj.to_dict()
# Use get_query_str_extended (single, clean statement) instead of
# get_query_str, which returns a multi-statement string (prequeries +
# main SQL joined by ";" with a trailing ";"). The base command runs the
# SQL through SQLAlchemy text(), which only accepts a single statement,
# so the multi-statement form fails on engines that emit prequeries
# (e.g. PostgreSQL/Snowflake "SET search_path"). Prequeries still run
# via the connect-event listener registered in Database.get_sqla_engine.
# get_query_str_extended lives on ExploreMixin, not the Explorable
# Protocol, so guard with getattr for datasources that lack it.
get_extended = getattr(datasource, "get_query_str_extended", None)
if callable(get_extended):
sql_query = get_extended(query_dict).sql
else:
sql_query = datasource.get_query_str(query_dict)
database = getattr(datasource, "database", None)
catalog = getattr(datasource, "catalog", None)
schema = getattr(datasource, "schema", None)
@@ -62,6 +62,21 @@ 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],
@@ -145,6 +160,8 @@ 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 []
@@ -170,7 +187,7 @@ def update_id_refs( # pylint: disable=too-many-locals # noqa: C901
return fixed
def update_cross_filter_scoping(
def update_cross_filter_scoping( # noqa: C901
config: dict[str, Any], id_map: dict[int, int]
) -> dict[str, Any]:
# fix cross filter references
@@ -185,6 +202,9 @@ def update_cross_filter_scoping(
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] = {}
@@ -212,6 +232,11 @@ def update_cross_filter_scoping(
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
+4 -1
View File
@@ -34,7 +34,10 @@ 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
+29 -1
View File
@@ -33,12 +33,20 @@ 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 ReportSchedule, ReportScheduleType, ReportState
from superset.reports.models import (
ReportCreationMethod,
ReportRecipientType,
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__)
@@ -89,6 +97,26 @@ 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(
+6
View File
@@ -74,6 +74,11 @@ class QueryContextFactory: # pylint: disable=too-few-public-methods
bool(form_data.get("server_pagination")) if form_data else False
)
# A "full" CSV/Excel export raises the row-limit ceiling to
# TABLE_VIZ_MAX_ROW_SERVER (when ALLOW_FULL_CSV_EXPORT is enabled).
# The marker is set by the frontend's "Export to full ..." actions.
full_export = bool(form_data.get("full_export")) if form_data else False
queries_ = [
self._process_query_object(
datasource_model_instance,
@@ -82,6 +87,7 @@ class QueryContextFactory: # pylint: disable=too-few-public-methods
result_type,
datasource=datasource,
server_pagination=server_pagination,
full_export=full_export,
**query_obj,
),
)
+9 -2
View File
@@ -58,6 +58,7 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
time_range: str | None = None,
time_shift: str | None = None,
server_pagination: bool | None = None,
full_export: bool | None = None,
**kwargs: Any,
) -> QueryObject:
datasource_model_instance = None
@@ -66,9 +67,12 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
processed_extras = self._process_extras(extras)
result_type = kwargs.setdefault("result_type", parent_result_type)
# Process row limit taking server pagination into account
# Process row limit taking server pagination and full export into account
row_limit = self._process_row_limit(
row_limit, result_type, server_pagination=server_pagination
row_limit,
result_type,
server_pagination=server_pagination,
full_export=full_export,
)
processed_time_range = self._process_time_range(
@@ -106,12 +110,14 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
row_limit: int | None,
result_type: ChartDataResultType,
server_pagination: bool | None = None,
full_export: bool | None = None,
) -> int:
"""Process row limit taking into account server pagination.
:param row_limit: The requested row limit
:param result_type: The type of result being processed
:param server_pagination: Whether server-side pagination is enabled
:param full_export: Whether this is a "full" CSV/Excel export request
:return: The processed row limit
"""
default_row_limit = (
@@ -122,6 +128,7 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
return apply_max_row_limit(
row_limit or default_row_limit,
server_pagination=server_pagination,
full_export=full_export,
)
@staticmethod
+7 -2
View File
@@ -1317,7 +1317,9 @@ MAPBOX_API_KEY = os.environ.get("MAPBOX_API_KEY", "")
# Maximum number of rows returned for any analytical database query
SQL_MAX_ROW = 100000
# Maximum number of rows for any query with Server Pagination in Table Viz type
# Maximum number of rows for any query with Server Pagination in Table Viz type.
# This also serves as the row-count ceiling for "full" CSV/Excel exports when the
# ALLOW_FULL_CSV_EXPORT feature flag is enabled (see apply_max_row_limit).
TABLE_VIZ_MAX_ROW_SERVER = 500000
@@ -2011,7 +2013,10 @@ 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. Use one of the following
# 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:
# firefox
# Requires: geckodriver and firefox installations
# Limitations: can be buggy at times
+40 -2
View File
@@ -21,10 +21,11 @@ import logging
import re
from datetime import datetime
from re import Pattern
from typing import Any, Optional, TYPE_CHECKING
from typing import Any, Callable, Optional, TYPE_CHECKING
from flask_babel import gettext as __
from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, JSON
from sqlalchemy import types
from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, INTERVAL, JSON
from sqlalchemy.dialects.postgresql.base import PGInspector
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.engine.url import URL
@@ -135,6 +136,34 @@ 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"""
@@ -526,8 +555,17 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec):
ENUM(),
GenericDataType.STRING,
),
(
re.compile(r"^interval", re.IGNORECASE),
INTERVAL(),
GenericDataType.NUMERIC,
),
)
column_type_mutators: dict[types.TypeEngine, Callable[[Any], Any]] = {
INTERVAL: _normalize_interval,
}
@classmethod
def get_schema_from_engine_params(
cls,
-348
View File
@@ -1,348 +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.
"""
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
]
+42 -23
View File
@@ -251,7 +251,7 @@ def _generate_ascii_line_chart(data: list[Any], width: int, height: int) -> str:
lines.append("" * min(width, 60))
# Extract values and labels for plotting
values, labels = _extract_time_series_data(data)
values, labels, is_temporal = _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)
trend_analysis = _analyze_trend(values, is_temporal=is_temporal)
lines.append("")
lines.append("📊 Trend Analysis:")
lines.extend(trend_analysis)
@@ -273,10 +273,17 @@ 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]]:
"""Extract time series data with labels."""
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.
"""
values = []
labels = []
is_temporal = False
for row in data[:20]: # Limit points for readability
if isinstance(row, dict):
@@ -299,6 +306,7 @@ def _extract_time_series_data(data: list[Any]) -> tuple[list[float], list[str]]:
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
@@ -306,7 +314,7 @@ def _extract_time_series_data(data: list[Any]) -> tuple[list[float], list[str]]:
values.append(numeric_val)
labels.append(label_val or f"P{len(values)}")
return values, labels
return values, labels, is_temporal
def _create_enhanced_line_chart(
@@ -407,8 +415,15 @@ def _draw_line_segment(
grid[y][x] = ""
def _analyze_trend(values: list[float]) -> list[str]:
"""Analyze trend in the data."""
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.
"""
if len(values) < 2:
return ["• Insufficient data for trend analysis"]
@@ -421,24 +436,28 @@ def _analyze_trend(values: list[float]) -> list[str]:
max_val = max(values)
avg_val = sum(values) / len(values)
# 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"
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}")
else:
trend_icon = "➡️"
trend_desc = "Relatively stable"
analysis.append("• ⚠️ Data is categorical — directional trend not meaningful")
analysis.append(f"{trend_icon} {trend_desc}")
analysis.append(f"• Range: {min_val:.1f} to {max_val:.1f} (avg: {avg_val:.1f})")
# Volatility
+101
View File
@@ -36,7 +36,10 @@ 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,
@@ -46,6 +49,31 @@ 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:
@@ -186,6 +214,19 @@ 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),
@@ -201,6 +242,19 @@ 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(
@@ -278,6 +332,53 @@ 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(
@@ -352,6 +352,20 @@ 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"
% (
@@ -402,7 +416,7 @@ async def generate_chart( # noqa: C901
)
execution_time = int((time.time() - start_time) * 1000)
error = ChartGenerationError(
error = compile_result.error_obj or ChartGenerationError(
error_type="compile_error",
message=(
"Chart query failed to execute. The chart was not saved."
@@ -532,7 +546,7 @@ async def generate_chart( # noqa: C901
)
execution_time = int((time.time() - start_time) * 1000)
error = ChartGenerationError(
error = compile_result.error_obj or ChartGenerationError(
error_type="compile_error",
message=(
"Chart query failed to execute. "
@@ -335,6 +335,15 @@ 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
@@ -405,6 +414,7 @@ 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,
)
@@ -418,6 +428,10 @@ 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),
+15 -1
View File
@@ -16,6 +16,7 @@
# under the License.
import logging
import secrets
import time
from collections import defaultdict
from typing import Any, Awaitable, Callable, Dict, Protocol, Sequence
@@ -245,11 +246,20 @@ 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
@@ -265,6 +275,7 @@ 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),
@@ -278,7 +289,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",
"success=%s, mcp_call_id=%s",
tool_name,
agent_id,
user_id,
@@ -288,6 +299,7 @@ class LoggingMiddleware(Middleware):
dataset_id,
duration_ms,
success,
mcp_call_id,
)
async def on_message(
@@ -391,8 +403,10 @@ 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)
@@ -50,6 +50,7 @@ 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__)
@@ -117,7 +118,50 @@ async def execute_sql(request: ExecuteSqlRequest, ctx: Context) -> ExecuteSqlRes
error_type=SupersetErrorType.DATABASE_SECURITY_ACCESS_ERROR.value,
)
# 2. Build QueryOptions and execute query
# 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
cache_opts = CacheOptions(force_refresh=True) if request.force_refresh else None
options = QueryOptions(
catalog=request.catalog,
@@ -129,11 +173,11 @@ async def execute_sql(request: ExecuteSqlRequest, ctx: Context) -> ExecuteSqlRes
cache=cache_opts,
)
# 3. Execute query
# 4. Execute query
with event_logger.log_context(action="mcp.execute_sql.query_execution"):
result = database.execute(request.sql, options)
# 4. Convert to MCP response format
# 5. Convert to MCP response format
with event_logger.log_context(action="mcp.execute_sql.response_conversion"):
response = _convert_to_response(result)
+8
View File
@@ -241,6 +241,14 @@ 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):
@@ -212,6 +212,7 @@ def _format_report(
environment: dict[str, str],
user_context: dict[str, Any],
timestamp: str,
mcp_call_id: str | None = None,
) -> str:
"""Render the final markdown report."""
lines: list[str] = [
@@ -224,6 +225,10 @@ def _format_report(
f"- **Platform:** {environment['platform']}",
f"- **User ID:** {user_context['user_id']}",
f"- **Roles:** {', '.join(user_context['roles']) or 'none'}",
]
if mcp_call_id:
lines.append(f"- **MCP Call ID:** {mcp_call_id}")
lines += [
"",
"## What the user was doing",
f"- **MCP tool:** {sanitized.get('tool_name') or 'not provided'}",
@@ -316,6 +321,7 @@ async def generate_bug_report(
environment=environment,
user_context=user_context,
timestamp=timestamp,
mcp_call_id=request.mcp_call_id,
)
return GenerateBugReportResponse(
+13 -1
View File
@@ -53,7 +53,11 @@ from superset_core.queries.models import (
)
from superset import security_manager
from superset.exceptions import SupersetParseError, SupersetSecurityException
from superset.exceptions import (
SupersetException,
SupersetParseError,
SupersetSecurityException,
)
from superset.explorables.base import TimeGrainDict
from superset.jinja_context import BaseTemplateProcessor, get_template_processor
from superset.models.helpers import (
@@ -99,6 +103,14 @@ class SqlTablesMixin: # pylint: disable=too-few-public-methods
)
except (SupersetSecurityException, SupersetParseError, TemplateError):
return []
except SupersetException as ex:
# Jinja macros such as ``{{ dataset(id) }}`` or ``{{ metric(...) }}``
# may reference resources that no longer exist (e.g. a deleted
# dataset). Surfacing the failure here would break list endpoints
# that include ``sql_tables`` in their payload, hiding every saved
# query from the user. Treat it as a parse failure instead.
logger.warning("Unable to extract tables from SQL via Jinja: %s", ex)
return []
class Query(
+78 -1
View File
@@ -49,13 +49,14 @@ from superset.databases.filters import DatabaseFilter
from superset.exceptions import SupersetException
from superset.extensions import event_logger
from superset.reports.filters import ReportScheduleAllTextFilter, ReportScheduleFilter
from superset.reports.models import ReportSchedule
from superset.reports.models import ReportCreationMethod, ReportSchedule
from superset.reports.schemas import (
get_delete_ids_schema,
get_slack_channels_schema,
openapi_spec_methods_override,
ReportSchedulePostSchema,
ReportSchedulePutSchema,
ReportScheduleSubscribeSchema,
)
from superset.utils.slack import get_channels_with_search
from superset.views.base_api import (
@@ -82,6 +83,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
RouteMethod.RELATED,
"bulk_delete",
"slack_channels", # not using RouteMethod since locally defined
"subscribe",
}
class_permission_name = "ReportSchedule"
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
@@ -198,6 +200,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
edit_columns = add_columns
add_model_schema = ReportSchedulePostSchema()
edit_model_schema = ReportSchedulePutSchema()
subscribe_schema = ReportScheduleSubscribeSchema()
order_columns = [
"active",
@@ -318,6 +321,80 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
)
return self.response_422(message=str(ex))
@expose("/subscribe", methods=("POST",))
@protect()
@statsd_metrics
@permission_name("subscribe")
@requires_json
def subscribe(self) -> Response:
"""Subscribe the current user to a chart or dashboard report.
---
post:
summary: Subscribe to a chart or dashboard report
description: >-
Creates a report schedule locked to the authenticated user's email.
``creation_method`` is derived server-side from the payload
(chart charts, dashboard dashboards). ``recipients`` are not
accepted and are always set to the requesting user's email address.
requestBody:
description: Report schedule subscription schema
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
responses:
201:
description: Report schedule subscription created
content:
application/json:
schema:
type: object
properties:
id:
type: number
result:
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
422:
$ref: '#/components/responses/422'
500:
$ref: '#/components/responses/500'
"""
try:
item = self.subscribe_schema.load(request.json)
except ValidationError as error:
return self.response_400(message=error.messages)
# Derive creation_method server-side from the payload
if item.get("dashboard") is not None:
item["creation_method"] = ReportCreationMethod.DASHBOARDS
elif item.get("chart") is not None:
item["creation_method"] = ReportCreationMethod.CHARTS
else:
return self.response_400(
message={"_schema": ["Either chart or dashboard is required"]}
)
try:
new_model = CreateReportScheduleCommand(item).run()
return self.response(201, id=new_model.id, result=item)
except ReportScheduleNotFoundError as ex:
return self.response_400(message=str(ex))
except ReportScheduleInvalidError as ex:
return self.response_422(message=ex.normalized_messages())
except ReportScheduleCreateFailedError as ex:
logger.error(
"Error creating report schedule %s: %s",
self.__class__.__name__,
str(ex),
exc_info=True,
)
return self.response_422(message=str(ex))
@expose("/", methods=("POST",))
@protect()
@statsd_metrics
+24
View File
@@ -305,6 +305,30 @@ class ReportSchedulePostSchema(Schema):
)
class ReportScheduleSubscribeSchema(ReportSchedulePostSchema):
"""Schema for creating a chart/dashboard subscription.
``recipients`` and ``creation_method`` are excluded both are set
server-side: recipients are locked to the authenticated user's email,
and creation_method is derived from the presence of ``chart`` or
``dashboard`` in the payload.
``type`` is restricted to ``Report`` alert schedules cannot be
created through the subscribe endpoint.
"""
type = fields.String(
metadata={"description": type_description},
allow_none=False,
required=True,
validate=validate.OneOf(choices=[ReportScheduleType.REPORT.value]),
)
class Meta:
exclude = ("recipients", "creation_method", "owners")
unknown = EXCLUDE
class ReportSchedulePutSchema(Schema):
type = fields.String(
metadata={"description": type_description},
+53
View File
@@ -439,6 +439,14 @@ class BaseSQLStatement(Generic[InternalRepresentation]):
"""
raise NotImplementedError()
def is_destructive(self) -> bool:
"""
Check if the statement is destructive DDL (DROP, TRUNCATE, ALTER).
:return: True if the statement is destructive DDL.
"""
raise NotImplementedError()
def optimize(self) -> BaseSQLStatement[InternalRepresentation]:
"""
Return optimized statement.
@@ -719,6 +727,31 @@ class SQLStatement(BaseSQLStatement[exp.Expression]):
return False
def is_destructive(self) -> bool:
"""
Check if the statement is destructive DDL (DROP, TRUNCATE, ALTER).
Unlike ``is_mutating()``, this excludes non-destructive DML
(INSERT, UPDATE, DELETE, MERGE) and CREATE.
:return: True if the statement is destructive DDL.
"""
destructive_nodes = (
exp.Drop,
exp.TruncateTable,
exp.Alter,
)
for node_type in destructive_nodes:
if self._parsed.find(node_type):
return True
# Handle ALTER parsed as Command (Oracle, MS SQL dialects)
if isinstance(self._parsed, exp.Command) and self._parsed.name == "ALTER":
return True # pragma: no cover
return False
def format(self, comments: bool = True) -> str:
"""
Pretty-format the SQL statement.
@@ -1175,6 +1208,18 @@ class KustoKQLStatement(BaseSQLStatement[str]):
"""
return self._parsed.startswith(".") and not self._parsed.startswith(".show")
def is_destructive(self) -> bool:
"""
Check if the statement is destructive DDL.
Kusto KQL uses dot-commands for management operations. Destructive
operations start with ``.drop`` or ``.alter``.
:return: True if the statement is destructive DDL.
"""
lower = self._parsed.lower()
return lower.startswith(".drop") or lower.startswith(".alter")
def optimize(self) -> KustoKQLStatement:
"""
Return optimized statement.
@@ -1321,6 +1366,14 @@ class SQLScript:
"""
return any(statement.is_mutating() for statement in self.statements)
def has_destructive(self) -> bool:
"""
Check if the script contains destructive DDL (DROP, TRUNCATE, ALTER).
:return: True if any statement is destructive DDL.
"""
return any(statement.is_destructive() for statement in self.statements)
def optimize(self) -> SQLScript:
"""
Return optimized script.
+21 -5
View File
@@ -2051,13 +2051,17 @@ def parse_boolean_string(bool_str: str | None) -> bool:
def apply_max_row_limit(
limit: int,
server_pagination: bool | None = None,
full_export: bool | None = None,
) -> int:
"""
Override row limit based on server pagination setting
Override row limit based on server pagination / full-export settings
:param limit: requested row limit
:param server_pagination: whether server-side pagination
is enabled, defaults to None
:param full_export: whether this is a "full" CSV/Excel export request,
which raises the ceiling to TABLE_VIZ_MAX_ROW_SERVER when the
ALLOW_FULL_CSV_EXPORT feature flag is enabled, defaults to None
:return: Capped row limit
>>> apply_max_row_limit(600000, server_pagination=True) # Server pagination
@@ -2069,13 +2073,25 @@ def apply_max_row_limit(
>>> apply_max_row_limit(0) # Zero returns default max limit
50000
"""
# Imported locally to avoid a circular import: superset.extensions pulls in
# superset.security.manager / superset.utils.cache_manager, both of which
# import superset.utils.core.
# pylint: disable=import-outside-toplevel
from superset.extensions import feature_flag_manager
max_limit = (
app.config["TABLE_VIZ_MAX_ROW_SERVER"]
if server_pagination
else app.config["SQL_MAX_ROW"]
# A "full" CSV/Excel export is allowed past the regular SQL_MAX_ROW cap, but
# only when the operator has opted in via the ALLOW_FULL_CSV_EXPORT flag.
# server_pagination is a separate, independent reason to raise the cap and is
# NOT gated by that flag.
allow_full_export = full_export and feature_flag_manager.is_feature_enabled(
"ALLOW_FULL_CSV_EXPORT"
)
# Both raised cases share the same ceiling, TABLE_VIZ_MAX_ROW_SERVER (a
# bounded, predictable maximum); see its definition in config.py.
if server_pagination or allow_full_export:
max_limit = app.config["TABLE_VIZ_MAX_ROW_SERVER"]
else:
max_limit = app.config["SQL_MAX_ROW"]
if limit != 0:
return min(max_limit, limit)
return max_limit
@@ -1237,6 +1237,76 @@ class TestChartApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCase):
chart["id"] for chart in data_by_name["result"]
), set(chart.id for chart in expected_charts) # noqa: C401
def test_get_charts_changed_on_delta_humanized_sort_monotonic(self):
"""Regression for #27500: sorting the chart list by
`changed_on_delta_humanized` desc must yield results whose underlying
`changed_on` timestamps are monotonically non-increasing. The original
report shows the humanized column visually out of order, suggesting
the sort key didn't actually reflect the timestamp."""
from datetime import datetime, timedelta
admin = self.get_user("admin")
# Insert two charts with distinct changed_on timestamps. Use raw UPDATE
# to force the values since assignment alone can be overridden by the
# before-update hook.
chart_older = self.insert_chart(
"regression_27500_older", [admin.id], 1, description="z"
)
chart_newer = self.insert_chart(
"regression_27500_newer", [admin.id], 1, description="z"
)
# Use timestamps whose humanized strings sort DIFFERENTLY from their
# real timestamps under a naive lexical sort. "3 hours ago" vs
# "5 hours ago": lexical-desc puts "5..." first (older), but
# timestamp-desc must put "3..." first (newer). If the API ever
# accidentally sorts by the humanized text instead of the column,
# this test fails. (Pairs like "now"/"2 days ago" don't discriminate
# because 'n' > '2' lexically agrees with newest-first.)
now = datetime.utcnow()
chart_older.changed_on = now - timedelta(hours=5)
chart_newer.changed_on = now - timedelta(hours=3)
db.session.commit()
try:
self.login(ADMIN_USERNAME)
arguments = {
"order_column": "changed_on_delta_humanized",
"order_direction": "desc",
"filters": [
{
"col": "slice_name",
"opr": "sw",
"value": "regression_27500_",
}
],
}
uri = f"api/v1/chart/?q={rison.dumps(arguments)}"
rv = self.get_assert_metric(uri, "get_list")
assert rv.status_code == 200
data = json.loads(rv.data.decode("utf-8"))
results = data["result"]
assert len(results) >= 2, f"expected at least 2 results, got {results}"
# The two inserted charts should appear in newest-first order.
indices = {
row["slice_name"]: i
for i, row in enumerate(results)
if row["slice_name"].startswith("regression_27500_")
}
ordering = [
row["slice_name"]
for row in results
if row["slice_name"].startswith("regression_27500_")
]
assert (
indices["regression_27500_newer"] < indices["regression_27500_older"]
), f"changed_on_delta_humanized desc sort is wrong: {ordering}; see #27500"
finally:
db.session.delete(chart_older)
db.session.delete(chart_newer)
db.session.commit()
def test_get_charts_changed_on(self):
"""
Dashboard API: Test get charts changed on
+2 -1
View File
@@ -337,7 +337,8 @@ class TestReportSchedulesApi(SupersetTestCase):
assert rv.status_code == 200
assert "can_read" in data["permissions"]
assert "can_write" in data["permissions"]
assert len(data["permissions"]) == 2
assert "can_subscribe" in data["permissions"]
assert len(data["permissions"]) == 3
@pytest.mark.usefixtures("create_report_schedules")
def test_get_report_schedule_not_found(self):
@@ -29,6 +29,7 @@ def _setup_chart_mocks(
sql: str = "SELECT * FROM test",
catalog: str | None = None,
schema: str | None = None,
prequeries: list[str] | None = None,
) -> tuple[MockerFixture, MockerFixture, MockerFixture]:
"""Set up common mocks for chart streaming export tests."""
mock_db = mocker.patch("superset.commands.streaming_export.base.db")
@@ -37,12 +38,22 @@ def _setup_chart_mocks(
query_context = mocker.MagicMock()
datasource = mocker.MagicMock()
datasource.get_query_str.return_value = sql
# The command prefers get_query_str_extended (clean single statement);
# get_query_str returns the legacy multi-statement form.
extended = mocker.MagicMock()
extended.sql = sql
extended.prequeries = prequeries or []
datasource.get_query_str_extended.return_value = extended
datasource.get_query_str.return_value = (
";\n\n".join((prequeries or []) + [sql]) + ";"
)
datasource.database = mocker.MagicMock()
datasource.catalog = catalog
datasource.schema = schema
query_context.datasource = datasource
query_context.queries = [mocker.MagicMock()]
query_obj = mocker.MagicMock()
query_obj.to_dict.return_value = {"row_limit": 100}
query_context.queries = [query_obj]
mock_session.merge.return_value = datasource.database
return mock_db, query_context, datasource
@@ -296,3 +307,39 @@ def test_catalog_and_schema_passed_to_engine(mocker: MockerFixture) -> None:
catalog="my_catalog",
schema="my_schema",
)
def test_uses_extended_sql_single_statement(mocker: MockerFixture) -> None:
"""SQL generation uses get_query_str_extended (no prequeries, no trailing ;).
get_query_str returns a multi-statement string that SQLAlchemy text()
rejects; the command must use the clean single-statement extended form.
"""
_, query_context, datasource = _setup_chart_mocks(
mocker,
sql="SELECT * FROM test",
prequeries=["SET search_path = my_schema"],
)
command = StreamingCSVExportCommand(query_context)
sql_query, _, _, _ = command._get_sql_and_database()
assert sql_query == "SELECT * FROM test"
assert ";\n\n" not in sql_query
assert not sql_query.endswith(";")
datasource.get_query_str.assert_not_called()
def test_falls_back_to_get_query_str_without_extended(
mocker: MockerFixture,
) -> None:
"""Datasources lacking get_query_str_extended fall back to get_query_str."""
_, query_context, datasource = _setup_chart_mocks(mocker)
# SemanticView and other Explorables may not implement the extended form.
del datasource.get_query_str_extended
command = StreamingCSVExportCommand(query_context)
sql_query, _, _, _ = command._get_sql_and_database()
datasource.get_query_str.assert_called_once()
assert "SELECT * FROM test" in sql_query
@@ -1478,3 +1478,46 @@ def test_success_state_report_sends_and_logs_success(
ReportState.SUCCESS,
error_message=None,
)
def test_get_url_for_csv_uses_post_processed_type(
app: SupersetApp,
mocker: MockerFixture,
) -> None:
"""Regression for #25538: when an alert/report generates a CSV for a
chart, the URL must request type=POST_PROCESSED so the chart's saved
filters (including time-range filters) are applied. The original report
described a chart with a "last 30 days" filter that returned only 14
rows in the UI, but the alert CSV came back with 219 rows (the entire
unfiltered table).
POST_PROCESSED is the marker that propagates the chart's query_context
-- including its time filter -- through to the CSV renderer. A
regression that switched to FULL or RAW would replicate the original
bug.
"""
from datetime import datetime
from uuid import UUID
from superset.commands.report.execute import BaseReportState
from superset.common.chart_data import ChartDataResultFormat
app.config.update({"ALERT_REPORTS_EXECUTORS": {}})
report_schedule = create_report_schedule(mocker)
report_schedule.force_screenshot = False
report_schedule.chart_id = report_schedule.chart.id
state = BaseReportState(
report_schedule=report_schedule,
scheduled_dttm=datetime.now(),
execution_id=UUID("084e7ee6-5557-4ecd-9632-b7f39c9ec524"),
)
url = state._get_url(result_format=ChartDataResultFormat.CSV)
assert "format=csv" in url.lower(), f"expected csv format in URL: {url}"
assert "type=post_processed" in url.lower(), (
f"CSV report URL must use type=post_processed so chart filters "
f"(incl. time filters) are applied; got: {url}; see issue #25538"
)
+145 -3
View File
@@ -14,11 +14,11 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Unit tests for UpdateReportScheduleCommand.validate() database invariants."""
"""Unit tests for UpdateReportScheduleCommand.validate()."""
from __future__ import annotations
from unittest.mock import Mock
from unittest.mock import Mock, patch
import pytest
from pytest_mock import MockerFixture
@@ -30,7 +30,11 @@ from superset.commands.report.exceptions import (
from superset.commands.report.update import UpdateReportScheduleCommand
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetSecurityException
from superset.reports.models import ReportScheduleType, ReportState
from superset.reports.models import (
ReportCreationMethod,
ReportScheduleType,
ReportState,
)
def _make_model(
@@ -38,10 +42,12 @@ def _make_model(
*,
model_type: ReportScheduleType | str,
database_id: int | None,
creation_method: ReportCreationMethod = ReportCreationMethod.ALERTS_REPORTS,
) -> Mock:
model = mocker.Mock()
model.type = model_type
model.database_id = database_id
model.creation_method = creation_method
model.name = "test_schedule"
model.crontab = "0 9 * * *"
model.last_state = "noop"
@@ -257,6 +263,142 @@ def test_report_to_alert_with_db_accepted(mocker: MockerFixture) -> None:
cmd.validate() # should not raise
# --- Recipient enforcement for chart/dashboard reports ---
_PATCH_GET_USER_EMAIL = "superset.commands.report.update.get_user_email"
def test_chart_report_update_recipient_overridden_with_owner_email(
mocker: MockerFixture,
) -> None:
"""Updating recipients on a chart report always locks them to the owner's email."""
model = _make_model(
mocker,
model_type=ReportScheduleType.REPORT,
database_id=None,
creation_method=ReportCreationMethod.CHARTS,
)
_setup_mocks(mocker, model)
data = {
"recipients": [
{
"type": "Email",
"recipient_config_json": {"target": "other@example.com"},
}
]
}
cmd = UpdateReportScheduleCommand(model_id=1, data=data)
with patch(_PATCH_GET_USER_EMAIL, return_value="owner@example.com"):
cmd.validate()
recipients = cmd._properties["recipients"]
assert len(recipients) == 1
assert recipients[0]["recipient_config_json"]["target"] == "owner@example.com"
def test_dashboard_report_update_recipient_overridden_with_owner_email(
mocker: MockerFixture,
) -> None:
"""Updating recipients on a dashboard report locks them to the owner's email."""
model = _make_model(
mocker,
model_type=ReportScheduleType.REPORT,
database_id=None,
creation_method=ReportCreationMethod.DASHBOARDS,
)
_setup_mocks(mocker, model)
data = {
"recipients": [
{
"type": "Email",
"recipient_config_json": {"target": "other@example.com"},
}
]
}
cmd = UpdateReportScheduleCommand(model_id=1, data=data)
with patch(_PATCH_GET_USER_EMAIL, return_value="owner@example.com"):
cmd.validate()
recipients = cmd._properties["recipients"]
assert len(recipients) == 1
assert recipients[0]["recipient_config_json"]["target"] == "owner@example.com"
def test_alerts_reports_update_recipient_not_overridden(
mocker: MockerFixture,
) -> None:
"""Recipients on admin-created alerts/reports are not modified on update."""
model = _make_model(
mocker,
model_type=ReportScheduleType.REPORT,
database_id=None,
creation_method=ReportCreationMethod.ALERTS_REPORTS,
)
_setup_mocks(mocker, model)
original_recipient = {
"type": "Email",
"recipient_config_json": {"target": "team@example.com"},
}
cmd = UpdateReportScheduleCommand(
model_id=1, data={"recipients": [original_recipient]}
)
with patch(_PATCH_GET_USER_EMAIL, return_value="owner@example.com"):
cmd.validate()
assert (
cmd._properties["recipients"][0]["recipient_config_json"]["target"]
== "team@example.com"
)
def test_chart_report_update_no_recipients_in_payload_unchanged(
mocker: MockerFixture,
) -> None:
"""If recipients are not in the update payload, nothing is changed."""
model = _make_model(
mocker,
model_type=ReportScheduleType.REPORT,
database_id=None,
creation_method=ReportCreationMethod.CHARTS,
)
_setup_mocks(mocker, model)
cmd = UpdateReportScheduleCommand(model_id=1, data={"name": "new name"})
with patch(_PATCH_GET_USER_EMAIL, return_value="owner@example.com"):
cmd.validate()
assert "recipients" not in cmd._properties
def test_chart_report_update_no_user_email_raises(mocker: MockerFixture) -> None:
"""Update fails with a validation error when the user has no email address."""
model = _make_model(
mocker,
model_type=ReportScheduleType.REPORT,
database_id=None,
creation_method=ReportCreationMethod.CHARTS,
)
_setup_mocks(mocker, model)
cmd = UpdateReportScheduleCommand(
model_id=1,
data={
"recipients": [
{"type": "Email", "recipient_config_json": {"target": "x@y.com"}}
]
},
)
with patch(_PATCH_GET_USER_EMAIL, return_value=None):
with pytest.raises(ReportScheduleInvalidError) as exc_info:
cmd.validate()
messages = _get_validation_messages(exc_info)
assert "recipients" in messages
# --- Deactivation state reset ---
@@ -30,6 +30,7 @@ def create_app_config() -> dict[str, Any]:
"DEFAULT_RELATIVE_END_TIME": "today",
"SAMPLES_ROW_LIMIT": 1000,
"SQL_MAX_ROW": 100000,
"TABLE_VIZ_MAX_ROW_SERVER": 500000,
}
@@ -48,12 +49,12 @@ def connector_registry() -> Mock:
def apply_max_row_limit(
limit: int,
server_pagination: bool | None = None,
full_export: bool | None = None,
) -> int:
max_limit = (
create_app_config()["TABLE_VIZ_MAX_ROW_SERVER"]
if server_pagination
else create_app_config()["SQL_MAX_ROW"]
)
if server_pagination or full_export:
max_limit = create_app_config()["TABLE_VIZ_MAX_ROW_SERVER"]
else:
max_limit = create_app_config()["SQL_MAX_ROW"]
if limit != 0:
return min(max_limit, limit)
return max_limit
@@ -109,6 +110,36 @@ class TestQueryObjectFactory:
assert query_object.row_limit == 100
assert query_object.row_offset == 200
def test_query_context_full_export_raises_limit(
self,
query_object_factory: QueryObjectFactory,
raw_query_context: dict[str, Any],
):
"""full_export raises the row-limit ceiling to TABLE_VIZ_MAX_ROW_SERVER."""
raw_query_object = raw_query_context["queries"][0]
raw_query_object["row_limit"] = 300000
query_object = query_object_factory.create(
raw_query_context["result_type"],
full_export=True,
**raw_query_object,
)
# Without full_export this would be capped at SQL_MAX_ROW (100000).
assert query_object.row_limit == 300000
def test_query_context_limit_capped_without_full_export(
self,
query_object_factory: QueryObjectFactory,
raw_query_context: dict[str, Any],
):
"""A regular request stays capped at SQL_MAX_ROW."""
raw_query_object = raw_query_context["queries"][0]
raw_query_object["row_limit"] = 300000
query_object = query_object_factory.create(
raw_query_context["result_type"],
**raw_query_object,
)
assert query_object.row_limit == 100000
def test_query_context_null_post_processing_op(
self,
query_object_factory: QueryObjectFactory,
@@ -137,3 +137,51 @@ def test_get_virtual_table_metadata_multiple(mocker: MockerFixture) -> None:
with pytest.raises(SupersetSecurityException) as excinfo:
get_virtual_table_metadata(dataset)
assert str(excinfo.value) == "Only single queries supported"
def test_get_virtual_table_metadata_renders_jinja(mocker: MockerFixture) -> None:
"""Regression for #25839: Jinja templates in a virtual dataset's SQL must
be rendered via the template processor before SQL parsing. Otherwise the
raw Jinja tokens reach sqlglot and the parser rejects them as a syntax
error (the user-visible symptom is "Invalid SQL" when clicking
"SYNC COLUMNS FROM SOURCE" on a dataset that uses {{ from_dttm }} etc.).
"""
mock_get_columns_description = mocker.patch(
"superset.connectors.sqla.utils.get_columns_description",
return_value=[{"name": "rendered_col", "type": "INTEGER"}],
)
raw_sql = "SELECT * FROM tbl WHERE ts > '{{ from_dttm }}'"
rendered_sql = "SELECT * FROM tbl WHERE ts > '2024-01-01 00:00:00'"
dataset = mocker.MagicMock(sql=raw_sql)
dataset.database.db_engine_spec.engine = "postgresql"
dataset.template_params_dict = {}
dataset.get_template_processor().process_template.return_value = rendered_sql
# If Jinja rendering is skipped, sqlglot tries to parse the raw {{ ... }}
# and raises SupersetGenericDBErrorException / SupersetParseError.
assert get_virtual_table_metadata(dataset) == [
{"name": "rendered_col", "type": "INTEGER"}
]
# The template processor MUST have been called with the raw SQL (the
# whole point of the bug fix). A future regression that re-introduces
# the "Jinja not rendered" path would either skip this call or call it
# with the wrong input.
dataset.get_template_processor().process_template.assert_any_call(
raw_sql, **dataset.template_params_dict
)
# End-to-end guard: the rendered SQL must reach get_columns_description,
# not the raw Jinja string. A regression where rendering is used for
# parsing only and the raw SQL leaks downstream would pass the
# process_template assertion above but fail this one.
call_args = mock_get_columns_description.call_args
assert call_args is not None, "get_columns_description was never called"
passed_query = call_args.kwargs.get("query")
if passed_query is None and call_args.args:
passed_query = call_args.args[-1]
assert passed_query == rendered_sql, (
f"get_columns_description received unrendered SQL: {passed_query!r}"
)
@@ -123,6 +123,251 @@ def test_update_native_filter_config_scope_excluded():
}
def test_update_native_filter_config_preserves_rootpath_and_remaps_excluded():
"""
Regression guard for #19944: a native filter's ``scope`` has two parts that
must both survive an export/import roundtrip:
- ``rootPath`` controls *which* dashboard sections (tabs/rows) the filter
applies to. It uses position keys (``ROOT_ID``, ``TAB-xxx``), not chart
IDs, so ``update_id_refs`` must leave it untouched.
- ``excluded`` is a list of chart IDs the filter does NOT apply to within
its rootPath. Those IDs must be remapped to destination-env IDs.
The original bug report "filters are automatically applied to all charts,
even if a different scoping was defined before the export" — describes a
rootPath silently being collapsed back to ``["ROOT_ID"]`` (i.e. "apply
everywhere"). This test pins the post-refactor contract: the import path
must not mutate or drop ``rootPath``.
"""
from superset.commands.dashboard.importers.v1.utils import update_id_refs
config: dict[str, Any] = {
"position": {
"CHART1": {
"id": "CHART1",
"meta": {"chartId": 101, "uuid": "uuid1"},
"type": "CHART",
},
"CHART2": {
"id": "CHART2",
"meta": {"chartId": 102, "uuid": "uuid2"},
"type": "CHART",
},
"CHART3": {
"id": "CHART3",
"meta": {"chartId": 103, "uuid": "uuid3"},
"type": "CHART",
},
},
"metadata": {
"native_filter_configuration": [
{
"id": "NATIVE_FILTER-region",
"name": "Region",
"scope": {
# Filter applies only to charts under TAB-revenue,
# except chart 102 which is explicitly excluded.
"rootPath": ["TAB-revenue"],
"excluded": [102],
},
},
{
"id": "NATIVE_FILTER-product",
"name": "Product",
"scope": {
# Different filter, different rootPath; must not be
# cross-contaminated with the first filter's scope.
"rootPath": ["TAB-inventory", "TAB-revenue"],
"excluded": [101, 103],
},
},
],
},
}
chart_ids = {"uuid1": 1, "uuid2": 2, "uuid3": 3}
dataset_info: dict[str, dict[str, Any]] = {}
fixed = update_id_refs(config, chart_ids, dataset_info)
filters = fixed["metadata"]["native_filter_configuration"]
# rootPath uses position keys, not chart IDs — must pass through unchanged.
assert filters[0]["scope"]["rootPath"] == ["TAB-revenue"]
assert filters[1]["scope"]["rootPath"] == ["TAB-inventory", "TAB-revenue"]
# excluded uses chart IDs — must be remapped to destination-env IDs.
assert filters[0]["scope"]["excluded"] == [2]
assert filters[1]["scope"]["excluded"] == [1, 3]
def test_update_native_filter_config_default_rootpath_preserved():
"""
The "apply everywhere" default ``rootPath: ["ROOT_ID"]`` must also
survive untouched. A regression that special-cased this value (e.g. by
deleting it) would silently change "apply everywhere" into "apply nowhere"
on import, since downstream consumers treat a missing rootPath as empty
rather than as the default.
"""
from superset.commands.dashboard.importers.v1.utils import update_id_refs
config: dict[str, Any] = {
"position": {
"CHART1": {
"id": "CHART1",
"meta": {"chartId": 101, "uuid": "uuid1"},
"type": "CHART",
},
},
"metadata": {
"native_filter_configuration": [
{
"id": "NATIVE_FILTER-global",
"name": "Global",
"scope": {"rootPath": ["ROOT_ID"], "excluded": []},
}
],
},
}
chart_ids = {"uuid1": 1}
dataset_info: dict[str, dict[str, Any]] = {}
fixed = update_id_refs(config, chart_ids, dataset_info)
scope = fixed["metadata"]["native_filter_configuration"][0]["scope"]
assert scope["rootPath"] == ["ROOT_ID"]
assert scope["excluded"] == []
def test_update_id_refs_remaps_charts_in_scope():
"""
Regression for #26338: ``chartsInScope`` on a native filter holds chart
IDs and must be remapped from source-env IDs to destination-env IDs
during import.
The export side already converts ``chartsInScope`` IDs to UUIDs (see
``export_example.py:325``). The import side must symmetrically convert
them back to the destination environment's chart IDs. Without that
remap, the field carries stale source IDs into the imported dashboard
and breaks ``filtersInScope`` / ``filtersOutScope`` computation
filters end up applied to the wrong charts (or none at all).
"""
from superset.commands.dashboard.importers.v1.utils import update_id_refs
config: dict[str, Any] = {
"position": {
"CHART1": {
"id": "CHART1",
"meta": {"chartId": 101, "uuid": "uuid1"},
"type": "CHART",
},
"CHART2": {
"id": "CHART2",
"meta": {"chartId": 102, "uuid": "uuid2"},
"type": "CHART",
},
},
"metadata": {
"native_filter_configuration": [
{
"id": "NATIVE_FILTER-region",
"scope": {"rootPath": ["ROOT_ID"], "excluded": []},
# chartsInScope contains source-env chart IDs.
"chartsInScope": [101, 102, 103],
}
],
},
}
chart_ids = {"uuid1": 1, "uuid2": 2}
dataset_info: dict[str, dict[str, Any]] = {}
fixed = update_id_refs(config, chart_ids, dataset_info)
filter_config = fixed["metadata"]["native_filter_configuration"][0]
# Resolved IDs are remapped; unknown IDs (103) are dropped rather than
# left to silently bind to whatever chart owns that integer in the
# destination environment.
assert filter_config["chartsInScope"] == [1, 2]
def test_update_id_refs_remaps_cross_filter_charts_in_scope():
"""
Companion to test_update_id_refs_remaps_charts_in_scope. Cross-filter
config also stores ``chartsInScope`` (under ``crossFilters`` per chart)
and must be remapped on import for the same reason.
"""
from superset.commands.dashboard.importers.v1.utils import update_id_refs
config: dict[str, Any] = {
"position": {
"CHART1": {
"id": "CHART1",
"meta": {"chartId": 101, "uuid": "uuid1"},
"type": "CHART",
},
"CHART2": {
"id": "CHART2",
"meta": {"chartId": 102, "uuid": "uuid2"},
"type": "CHART",
},
},
"metadata": {
"chart_configuration": {
"101": {
"id": 101,
"crossFilters": {
"scope": {"rootPath": ["ROOT_ID"], "excluded": []},
"chartsInScope": [101, 102, 103],
},
},
},
},
}
chart_ids = {"uuid1": 1, "uuid2": 2}
dataset_info: dict[str, dict[str, Any]] = {}
fixed = update_id_refs(config, chart_ids, dataset_info)
cross_filters = fixed["metadata"]["chart_configuration"]["1"]["crossFilters"]
assert cross_filters["chartsInScope"] == [1, 2]
def test_update_id_refs_remaps_global_chart_configuration_charts_in_scope():
"""
Per-chart and native-filter ``chartsInScope`` are remapped by their own
branches; ``global_chart_configuration.chartsInScope`` lives next to
``global_chart_configuration.scope.excluded`` and needs the same treatment
so the global cross-filter scope cache doesn't keep stale source-env IDs.
"""
from superset.commands.dashboard.importers.v1.utils import update_id_refs
config: dict[str, Any] = {
"position": {
"CHART1": {
"id": "CHART1",
"meta": {"chartId": 101, "uuid": "uuid1"},
"type": "CHART",
},
"CHART2": {
"id": "CHART2",
"meta": {"chartId": 102, "uuid": "uuid2"},
"type": "CHART",
},
},
"metadata": {
"global_chart_configuration": {
"scope": {"rootPath": ["ROOT_ID"], "excluded": []},
"chartsInScope": [101, 102, 103],
},
},
}
chart_ids = {"uuid1": 1, "uuid2": 2}
dataset_info: dict[str, dict[str, Any]] = {}
fixed = update_id_refs(config, chart_ids, dataset_info)
assert fixed["metadata"]["global_chart_configuration"]["chartsInScope"] == [1, 2]
def test_update_id_refs_cross_filter_chart_configuration_key_and_excluded_mapping():
from superset.commands.dashboard.importers.v1.utils import update_id_refs
@@ -15,14 +15,14 @@
# specific language governing permissions and limitations
# under the License.
from datetime import datetime
from datetime import datetime, timedelta
from typing import Any, Optional
from unittest.mock import MagicMock
import pytest
from pytest_mock import MockerFixture
from sqlalchemy import column, types
from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, JSON
from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, INTERVAL, JSON
from sqlalchemy.engine.interfaces import Dialect
from sqlalchemy.engine.url import make_url
@@ -87,6 +87,8 @@ def test_convert_dttm(
("TIME", types.Time, None, GenericDataType.TEMPORAL, True),
# Boolean
("BOOLEAN", types.Boolean, None, GenericDataType.BOOLEAN, False),
# Interval (mapped to NUMERIC for chart rendering)
("INTERVAL", INTERVAL, None, GenericDataType.NUMERIC, False),
],
)
def test_get_column_spec(
@@ -366,3 +368,38 @@ class TestRedshiftDetection:
spec.update_params_from_encrypted_extra(database, params)
assert "pool_events" not in params
def test_interval_type_mutator() -> None:
"""
DB Eng Specs (postgres): Test INTERVAL type mutator
INTERVAL values are converted to milliseconds so users can apply
the built-in "DURATION" number format for human-readable display.
"""
mutator = spec.column_type_mutators[INTERVAL]
# Timedelta conversion — the only path psycopg2/psycopg3 actually
# exercises. Result is in milliseconds for compatibility with the
# DURATION formatter.
td = timedelta(days=1, hours=2, minutes=30, seconds=45)
assert mutator(td) == 95445000.0 # (1*86400 + 2*3600 + 30*60 + 45) * 1000
# Zero duration
assert mutator(timedelta(0)) == 0.0
# Negative interval
assert mutator(timedelta(days=-1)) == -86400000.0
# None preserves NULL semantics (not converted to 0)
assert mutator(None) is None
# Unexpected non-timedelta types fall through to the defensive
# `return None` (and emit a warning) rather than producing a
# mixed-type column.
assert mutator("1 day 02:30:45") is None
assert mutator("P1DT2H30M45S") is None
assert mutator(12345) is None
assert mutator(True) is None
assert mutator([1, 2, 3]) is None
assert mutator({"days": 1}) is None
@@ -1,413 +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.
# pylint: disable=import-outside-toplevel
from typing import Any
from unittest.mock import MagicMock, patch
from sqlalchemy.engine.url import make_url
from superset.utils import json
def _columns() -> list[dict[str, Any]]:
return [
{
"name": "region",
"type": "TEXT",
"nullable": True,
"default": None,
"comment": "dimension",
},
{
"name": "total_revenue",
"type": "FLOAT",
"nullable": True,
"default": None,
"comment": "metric",
"computed": {"sqltext": "total_revenue", "persisted": True},
},
]
def test_engine_spec_identity() -> None:
"""
The engine and dialect names line up with the shillelagh dialect.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
assert SemanticAPIEngineSpec.engine == "semanticapi"
assert SemanticAPIEngineSpec.engine_name == "Semantic Layer API"
assert "semanticapi://" in SemanticAPIEngineSpec.sqlalchemy_uri_placeholder
def test_select_star_returns_warning() -> None:
"""
Data preview is replaced with a single-row warning message.
"""
from superset.db_engine_specs.semantic_api import (
SELECT_STAR_MESSAGE,
SemanticAPIEngineSpec,
)
sql = SemanticAPIEngineSpec.select_star()
assert sql.startswith("SELECT '")
assert sql.endswith("' AS warning")
# The single-quote escape is preserved verbatim.
assert SELECT_STAR_MESSAGE.replace("'", "''") in sql
def test_get_columns_filters_metrics() -> None:
"""
Only non-computed columns (dimensions) flow through ``get_columns``.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
inspector = MagicMock()
inspector.get_columns.return_value = _columns()
table = MagicMock()
table.table = "sales"
table.schema = None
columns = SemanticAPIEngineSpec.get_columns(inspector, table)
assert [c["name"] for c in columns] == ["region"]
assert columns[0]["column_name"] == "region"
def test_adjust_engine_params_folds_extra_into_url() -> None:
"""
``additional_configuration`` from ``connect_args`` ends up on the URL query.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
uri, connect_args = SemanticAPIEngineSpec.adjust_engine_params(
make_url("sqlite://"),
{"additional_configuration": {"workspace": "acme"}, "other": 1},
)
assert "additional_configuration" not in connect_args
assert connect_args["other"] == 1
assert json.loads(uri.query["additional_configuration"]) == {"workspace": "acme"}
def test_adjust_engine_params_string_passthrough() -> None:
"""
A pre-serialised string config is forwarded verbatim.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
uri, _ = SemanticAPIEngineSpec.adjust_engine_params(
make_url("sqlite://"),
{"additional_configuration": '{"workspace":"acme"}'},
)
assert uri.query["additional_configuration"] == '{"workspace":"acme"}'
def test_adjust_engine_params_no_extra() -> None:
"""
Without ``additional_configuration`` the URL is untouched.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
uri, connect_args = SemanticAPIEngineSpec.adjust_engine_params(
make_url("sqlite://"),
{"other": 1},
)
assert uri.query == {}
assert connect_args == {"other": 1}
def test_supports_oauth2_flag() -> None:
"""
The engine spec advertises OAuth2 support and treats ``UnauthenticatedError``
as the trigger to start the dance.
"""
from shillelagh.exceptions import UnauthenticatedError
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
from superset.exceptions import OAuth2TokenRefreshError
assert SemanticAPIEngineSpec.supports_oauth2 is True
assert UnauthenticatedError in SemanticAPIEngineSpec.oauth2_exception
assert OAuth2TokenRefreshError in SemanticAPIEngineSpec.oauth2_exception
assert SemanticAPIEngineSpec.encrypted_extra_sensitive_fields == {
"$.oauth2_client_info.secret": "OAuth2 Client Secret",
}
def test_needs_oauth2_for_unauthenticated() -> None:
"""
``UnauthenticatedError`` with a logged-in user starts the dance.
"""
from shillelagh.exceptions import UnauthenticatedError
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
fake_g = MagicMock()
fake_g.user = MagicMock()
with patch("superset.db_engine_specs.semantic_api.g", fake_g):
assert (
SemanticAPIEngineSpec.needs_oauth2(UnauthenticatedError("expired")) is True
)
def test_needs_oauth2_ignores_unrelated_exceptions() -> None:
"""
Unrelated exception types don't trigger the dance.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
fake_g = MagicMock()
fake_g.user = MagicMock()
with patch("superset.db_engine_specs.semantic_api.g", fake_g):
assert SemanticAPIEngineSpec.needs_oauth2(RuntimeError("boom")) is False
def test_needs_oauth2_requires_user_context() -> None:
"""
Outside a request (no ``g.user``) the dance is not started.
"""
from shillelagh.exceptions import UnauthenticatedError
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
class _NoUserG:
def __bool__(self) -> bool:
return True
with patch("superset.db_engine_specs.semantic_api.g", _NoUserG()):
assert (
SemanticAPIEngineSpec.needs_oauth2(UnauthenticatedError("expired")) is False
)
def test_impersonate_user_injects_access_token() -> None:
"""
With a cached OAuth2 token the URL gains ``?access_token=``.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
url, engine_kwargs = SemanticAPIEngineSpec.impersonate_user(
database=MagicMock(),
username=None,
user_token="demo-access-token", # noqa: S106
url=make_url("sqlite://"),
engine_kwargs={"connect_args": {}},
)
assert url.query["access_token"] == "demo-access-token" # noqa: S105
assert engine_kwargs == {"connect_args": {}}
def test_impersonate_user_without_token_is_a_noop() -> None:
"""
Without a token, neither the URL nor engine_kwargs are changed.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
original_url = make_url("sqlite://")
url, engine_kwargs = SemanticAPIEngineSpec.impersonate_user(
database=MagicMock(),
username=None,
user_token=None,
url=original_url,
engine_kwargs={},
)
assert url is original_url
assert engine_kwargs == {}
def test_build_sqlalchemy_uri_minimum() -> None:
"""
Just host ``semanticapi://host/``.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
assert (
SemanticAPIEngineSpec.build_sqlalchemy_uri(
{"host": "localhost"},
)
== "semanticapi://localhost"
)
def test_build_sqlalchemy_uri_full() -> None:
"""
Host, port, secure, ``additional_configuration`` and OAuth client info all
round-trip cleanly. The OAuth2 URIs get auto-filled from host:port.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
encrypted_extra = {
"oauth2_client_info": {"id": "demo-client", "secret": "demo-secret"},
}
uri = SemanticAPIEngineSpec.build_sqlalchemy_uri(
{
"host": "h",
"port": 8000,
"secure": True,
"additional_configuration": {"workspace": "acme"},
},
encrypted_extra,
)
parsed = make_url(uri)
assert parsed.host == "h"
assert parsed.port == 8000
assert parsed.query["secure"] == "true"
assert json.loads(parsed.query["additional_configuration"]) == {"workspace": "acme"}
oauth2 = encrypted_extra["oauth2_client_info"]
assert oauth2["authorization_request_uri"] == "https://h:8000/authorize"
assert oauth2["token_request_uri"] == "https://h:8000/token" # noqa: S105
assert oauth2["scope"] == ""
def test_build_sqlalchemy_uri_oauth_uri_overrides_preserved() -> None:
"""
Existing OAuth URIs are left alone (no clobbering of explicit overrides).
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
encrypted_extra = {
"oauth2_client_info": {
"id": "x",
"secret": "y",
"authorization_request_uri": "https://idp/authorize",
"token_request_uri": "https://idp/token",
},
}
SemanticAPIEngineSpec.build_sqlalchemy_uri({"host": "h"}, encrypted_extra)
oauth2 = encrypted_extra["oauth2_client_info"]
assert oauth2["authorization_request_uri"] == "https://idp/authorize"
assert oauth2["token_request_uri"] == "https://idp/token" # noqa: S105
def test_build_sqlalchemy_uri_additional_configuration_string() -> None:
"""
A pre-serialised ``additional_configuration`` string is forwarded as-is.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
uri = SemanticAPIEngineSpec.build_sqlalchemy_uri(
{"host": "h", "additional_configuration": '{"x":1}'}, # type: ignore[typeddict-item]
)
assert make_url(uri).query["additional_configuration"] == '{"x":1}'
def test_get_parameters_from_uri_roundtrip() -> None:
"""
A URL produced by ``build_sqlalchemy_uri`` is parsed back to the same parameters.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
encrypted_extra = {"oauth2_client_info": {"id": "x", "secret": "y"}}
uri = SemanticAPIEngineSpec.build_sqlalchemy_uri(
{
"host": "h",
"port": 8000,
"secure": True,
"additional_configuration": {"workspace": "acme"},
},
encrypted_extra,
)
params = SemanticAPIEngineSpec.get_parameters_from_uri(uri, encrypted_extra)
assert params["host"] == "h"
assert params["port"] == 8000
assert params["secure"] is True
assert params["additional_configuration"] == {"workspace": "acme"}
oauth2 = params["oauth2_client_info"]
assert oauth2 is not None
assert oauth2["id"] == "x"
def test_get_parameters_from_uri_invalid_additional_configuration() -> None:
"""
Garbage in ``additional_configuration`` doesn't blow up — set to None.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
params = SemanticAPIEngineSpec.get_parameters_from_uri(
"semanticapi://h/?additional_configuration=not-json",
)
assert params["additional_configuration"] is None
def test_validate_parameters_missing_host() -> None:
"""
A missing host produces a clear validation error.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
errors = SemanticAPIEngineSpec.validate_parameters({"parameters": {}})
assert len(errors) == 1
extra = errors[0].extra
assert extra is not None
assert extra["missing"] == ["host"]
def test_validate_parameters_happy() -> None:
"""
With a host, no errors are reported.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
assert (
SemanticAPIEngineSpec.validate_parameters(
{"parameters": {"host": "localhost"}},
)
== []
)
def test_parameters_json_schema_exposes_fields() -> None:
"""
``parameters_json_schema`` advertises every form field so the frontend
can render it.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
schema = SemanticAPIEngineSpec.parameters_json_schema()
properties = schema["properties"]
assert set(properties).issuperset(
{"host", "port", "secure", "additional_configuration", "oauth2_client_info"},
)
# oauth2_client_info is marked as encrypted so the frontend stores it in
# ``encrypted_extra``.
assert properties["oauth2_client_info"].get("x-encrypted-extra") is True
def test_get_metrics_extracts_computed() -> None:
"""
Computed columns become Superset metric definitions.
"""
from superset.db_engine_specs.semantic_api import SemanticAPIEngineSpec
inspector = MagicMock()
inspector.get_columns.return_value = _columns()
table = MagicMock()
table.table = "sales"
table.schema = None
metrics = SemanticAPIEngineSpec.get_metrics(MagicMock(), inspector, table)
assert metrics == [
{
"metric_name": "total_revenue",
"expression": "total_revenue",
"description": "metric",
},
]
@@ -15,9 +15,88 @@
# specific language governing permissions and limitations
# under the License.
"""Unit tests for ascii_charts.py NaN/null value handling."""
"""Unit tests for ASCII charts: trend analysis, NaN/null handling,
and boolean skipping."""
from superset.mcp_service.chart.ascii_charts import generate_ascii_chart
from superset.mcp_service.chart.ascii_charts import (
_analyze_trend,
_extract_time_series_data,
generate_ascii_chart,
)
def test_analyze_trend_temporal_upward():
values = [100.0, 110.0, 120.0, 130.0, 150.0]
result = _analyze_trend(values, is_temporal=True)
trend_line = result[0]
assert "upward trend" in trend_line.lower()
assert "categorical" not in trend_line.lower()
def test_analyze_trend_temporal_downward():
values = [200.0, 180.0, 150.0, 120.0, 100.0]
result = _analyze_trend(values, is_temporal=True)
trend_line = result[0]
assert "downward trend" in trend_line.lower()
def test_analyze_trend_temporal_stable():
values = [100.0, 101.0, 100.5, 100.2, 100.8]
result = _analyze_trend(values, is_temporal=True)
trend_line = result[0]
assert "stable" in trend_line.lower()
def test_analyze_trend_categorical_skips_direction():
values = [200.0, 180.0, 150.0, 120.0, 100.0]
result = _analyze_trend(values, is_temporal=False)
trend_line = result[0]
assert "categorical" in trend_line.lower()
assert "downward" not in trend_line.lower()
assert "upward" not in trend_line.lower()
def test_analyze_trend_categorical_keeps_range_and_volatility():
values = [200.0, 180.0, 150.0, 120.0, 100.0]
result = _analyze_trend(values, is_temporal=False)
full_text = "\n".join(result)
assert "Range:" in full_text
assert "Volatility:" in full_text
def test_analyze_trend_defaults_to_temporal():
values = [100.0, 150.0, 200.0]
result = _analyze_trend(values)
trend_line = result[0]
assert "categorical" not in trend_line.lower()
assert "upward trend" in trend_line.lower()
def test_extract_time_series_data_temporal_column():
data = [
{"date": "2024-01-01", "value": 100},
{"date": "2024-02-01", "value": 200},
]
values, labels, is_temporal = _extract_time_series_data(data)
assert values == [100, 200]
assert is_temporal is True
def test_extract_time_series_data_categorical_column():
data = [
{"country": "Argentina", "value": 500},
{"country": "Brazil", "value": 300},
]
values, labels, is_temporal = _extract_time_series_data(data)
assert values == [500, 300]
assert is_temporal is False
def test_extract_time_series_data_time_keyword_variants():
for key in ["timestamp", "created_time", "month_name", "day_of_week", "year_end"]:
data = [{key: "val", "metric": 42}]
_, _, is_temporal = _extract_time_series_data(data)
assert is_temporal is True, f"Expected temporal for column '{key}'"
def test_bar_chart_with_null_values_does_not_raise() -> None:
@@ -423,6 +423,93 @@ class TestValidateAndCompileTier2:
assert result.error_code == "DATASET_NOT_FOUND"
@patch("superset.daos.dataset.DatasetDAO")
@patch("superset.commands.chart.data.get_data_command.ChartDataCommand")
@patch("superset.common.query_context_factory.QueryContextFactory")
def test_compile_chart_returns_database_error_when_wrapped_in_query_failed(
mock_factory, mock_cmd_cls, mock_dataset_dao
):
"""ChartDataCommand converts OperationalError to a string inside
ChartDataQueryFailedError (no __cause__ set). _classify_as_database_error
should use db_engine_spec.extract_errors() to detect the DB error."""
from superset.commands.chart.exceptions import ChartDataQueryFailedError
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.mcp_service.chart.compile import _compile_chart
mock_factory.return_value.create.return_value = Mock()
mock_cmd_cls.return_value.validate.return_value = None
# Real scenario: __cause__ is NOT set, error is just a string
wrapped = ChartDataQueryFailedError(
"Error: (psycopg2.OperationalError) connection to server at '10.0.0.1',"
" port 5432 failed: FATAL: tenant not found"
)
mock_cmd_cls.return_value.run.side_effect = wrapped
# Mock the dataset's db_engine_spec to return GENERIC_DB_ENGINE_ERROR
mock_db = Mock()
mock_db.db_engine_spec.extract_errors.return_value = [
SupersetError(
error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
message="connection to server failed",
level=ErrorLevel.ERROR,
extra={"engine_name": "PostgreSQL"},
)
]
mock_dataset = Mock()
mock_dataset.database = mock_db
mock_dataset_dao.find_by_id.return_value = mock_dataset
result = _compile_chart(
form_data={
"metrics": [{"label": "count", "expressionType": "SIMPLE"}],
"adhoc_filters": [],
},
dataset_id=1,
)
assert not result.success
assert "Database connection error" in result.error
assert result.error_obj is not None
assert result.error_obj.error_type == "database_connection_error"
assert result.error_obj.error_code == "DATABASE_CONNECTION_ERROR"
mock_db.db_engine_spec.extract_errors.assert_called_once()
@patch("superset.commands.chart.data.get_data_command.ChartDataCommand")
@patch("superset.common.query_context_factory.QueryContextFactory")
def test_compile_chart_returns_database_error_on_raw_sqlalchemy_error(
mock_factory, mock_cmd_cls
):
"""When SQLAlchemyError escapes unwrapped, _compile_chart should
catch it and return a database_connection_error."""
from sqlalchemy.exc import OperationalError
from superset.mcp_service.chart.compile import _compile_chart
mock_factory.return_value.create.return_value = Mock()
mock_cmd_cls.return_value.validate.return_value = None
mock_cmd_cls.return_value.run.side_effect = OperationalError(
"connection to server at '10.0.0.1', port 5432 failed: Connection timed out",
None,
None,
)
result = _compile_chart(
form_data={
"metrics": [{"label": "count", "expressionType": "SIMPLE"}],
"adhoc_filters": [],
},
dataset_id=1,
)
assert not result.success
assert "Database connection error" in result.error
assert result.error_obj is not None
assert result.error_obj.error_type == "database_connection_error"
assert result.error_obj.error_code == "DATABASE_CONNECTION_ERROR"
@pytest.mark.parametrize(
"config_factory",
[
@@ -20,6 +20,7 @@ Unit tests for dashboard generation MCP tools
"""
import logging
from datetime import datetime
from importlib import import_module
from unittest.mock import Mock, patch
@@ -117,8 +118,8 @@ def _mock_dashboard(id: int = 1, title: str = "Test Dashboard") -> Mock:
dashboard.slug = f"test-dashboard-{id}"
dashboard.description = "Test dashboard description"
dashboard.published = True
dashboard.created_on = "2024-01-01"
dashboard.changed_on = "2024-01-01"
dashboard.created_on = datetime(2024, 1, 1)
dashboard.changed_on = datetime(2024, 1, 1)
dashboard.created_by = Mock()
dashboard.created_by.username = "test_user"
dashboard.changed_by = Mock()

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