Compare commits

...
Author SHA1 Message Date
Enzo MartellucciandClaude Sonnet 5 5a6c1b977b refactor(sqllab): drop the redundant canSaveDataset prop from SaveQuery
SaveQuery already receives the query's result columns and computes its
own SQL-staleness check, so master's canSaveDataset prop (threaded
through SqlEditor -> SaveQuery -> SaveDatasetActionButton) duplicated
the same "did the query succeed" condition. Fold it into the single
check SaveQuery already owns instead of ANDing two independently
computed booleans together.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 14:17:12 +02:00
Enzo Martellucci e8577368d3 Merge branch 'master' into enxdev/fix/sql-lab-save-dataset
# Conflicts:
#	superset-frontend/src/SqlLab/components/SaveDatasetActionButton/SaveDatasetActionButton.test.tsx
#	superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
#	superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
2026-08-21 14:15:55 +02:00
Mehmet Salih Yavuz 65910abf21 fix(sqllab): disable Save dataset until the query runs successfully (#43330) 2026-08-21 12:56:42 +03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6e22050b50 chore(deps): bump the storybook group in /docs with 2 updates (#43377)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 01:10:41 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 132340b652 chore(deps): bump astral-sh/setup-uv from 10.0.0 to 10.0.1 (#43378)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 01:10:37 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 14408eb3db chore(deps-dev): bump the storybook group in /superset-frontend with 5 updates (#43379)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 01:10:33 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 87743ef3f8 chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /superset-frontend (#43380)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 01:10:29 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fb53761ec8 chore(deps-dev): bump baseline-browser-mapping from 2.11.13 to 2.11.14 in /superset-frontend (#43381)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 01:10:25 -07:00
Amin Ghadersohi f7d505e1fd fix(listview): stop card clicks creating a duplicate history entry (#43310) 2026-08-20 23:23:46 -04:00
22396d504a fix(ux): use title case for button labels (#40048)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-20 20:14:33 -07:00
Gaston LaterzaandClaude Opus 5 27ea5de44a fix(i18n-es): correct case/number collapse in the semantic-layer labels (#43311)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 20:03:57 -07:00
Joe LiandClaude Sonnet 5 18fc2c6228 fix(sqllab): stop copying a permalink when opening a saved query (#43147)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 15:44:20 -07:00
Amin Ghadersohi 18a36d04c7 fix(mcp): preserve user-authored result values (#43202) 2026-08-20 18:29:39 -04:00
ʈᵃᵢ 01b1d58ac9 fix(plugin-chart-echarts): restore tooltips for metrics labelled like… (#43369) 2026-08-20 14:58:11 -07:00
Joe Li 7441ce90ae fix(charts): align grain-less time comparisons safely (#43315) 2026-08-20 14:55:01 -07:00
Amin Ghadersohi 42ba2a4433 fix(reports): humanize day-of-month + day-of-week crontabs as OR (#43307) 2026-08-20 16:46:08 -04:00
madhushreeagandmadhushree agarwal 271564cb0d feat(config): add EXTRA_PANDAS_POSTPROCESSING_OPS extension point (#43337)
Co-authored-by: madhushree agarwal <madhushree_agarwal@apple.com>
2026-08-20 13:15:41 -07:00
JUST.in DO ITandClaude Sonnet 5 bcfb4346f6 fix(mcp): honor use_cache and cache_timeout in get_chart_data (#43349)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 13:01:41 -07:00
Joe Li b5fe00b407 test(frontend): shrink flaky/misplaced recently-archived e2e coverage to Jest unit tests (#43264) 2026-08-20 11:48:02 -07:00
Parman MohammadalizadehandEvan Rusackas 148ffaff50 fix(plugin-chart-echarts): omit stacked value labels on zero-height segments (#42756)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-20 11:04:19 -07:00
Ankit 83c6ea4e03 fix(explore): show the empty state when Samples returns no result payload (#43115) 2026-08-20 11:00:54 -07:00
Hans Yu 1ca5e61f54 chore: Unset future flag in engines and sessions. (#43273) 2026-08-20 10:58:37 -07:00
rlei 1ef12580a7 fix(chart): ignore chart actions for a chart no longer in state (#43228) 2026-08-20 10:57:21 -07:00
c0884c0f0c fix(explore): keep x-axis label when overriding Time Column with time comparison (#42875)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-20 10:52:22 -07:00
Sepuri Sai KrishnaandClaude Opus 5 2ff79bd495 fix(github): point the issue templates at labels that exist (#43357)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:13:36 -07:00
e7dccd44a7 fix(reports): prevent blank/partial report PDFs from virtualized charts (#43348)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-08-20 09:10:30 -04:00
dependabot[bot] fdd3945dde chore(deps): bump github/codeql-action/analyze from 4.37.6 to 4.37.7 (#43361)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-20 13:58:31 +03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8b67fb4d30 chore(deps-dev): bump globals from 17.10.0 to 17.11.0 in /superset-websocket (#43360)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:49 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b288db91f9 chore(deps): bump github/codeql-action/init from 4.37.6 to 4.37.7 (#43362)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:43 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 80bba12d0f chore(deps-dev): bump globals from 17.10.0 to 17.11.0 in /docs (#43363)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:38 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5d4358a149 chore(deps): bump google-auth-library from 11.0.1 to 11.0.2 in /superset-frontend (#43364)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:25 -07:00
f9cedf84e2 fix: drop post-processing options the operation no longer accepts (#42927)
Signed-off-by: Arya Ketan <aryaketan@sharechat.co>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-19 18:15:15 -07:00
Grégoire GaillyandEvan Rusackas c2d653b4b8 fix: set maxHeight of List components to height when in AutoSizer (#43056)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-19 16:56:30 -07:00
Đỗ Trọng HảiandJoe Li 5a96c3f538 chore(ci): disable Git commit info capture in Playwright E2E tests to avoid timeout (#43213)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-19 16:54:44 -07:00
ʈᵃᵢ faf7c34c0a fix(explore): legacy boolean filters and limit available operators based on calculated column type (#43341) 2026-08-19 15:37:09 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b8fca2145d chore(deps): bump astral-sh/setup-uv from 9.0.0 to 10.0.0 (#43322)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 14:40:02 -07:00
Alejandro Solares c10054f521 fix(plugin-chart-chord): declare react as a peerDependency (#43304) 2026-08-19 17:35:38 -04:00
Amin GhadersohiandClaude 8c500ccee1 fix(users): show password validation errors (#43191)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-19 16:57:46 -04:00
Joe LiandClaude Sonnet 5 6d77efad29 fix(chart): stop contextmenu propagation in BigNumberViz (#43267)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 13:57:28 -07:00
Amin GhadersohiandClaude 8222db3340 fix(dataset): preserve legacy default dashboard URLs (#43190)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-19 14:44:54 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 01ce8358a6 chore(deps-dev): bump globals from 17.9.0 to 17.10.0 in /superset-websocket (#43321)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 11:36:48 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 53a8a0e140 chore(deps): bump the docusaurus-openapi group in /docs with 2 updates (#43323)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 11:36:44 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> eafbff9f8d chore(deps-dev): bump globals from 17.9.0 to 17.10.0 in /docs (#43324)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 11:36:37 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1339bcd9da chore(deps): bump swagger-ui-react from 5.32.12 to 5.32.13 in /docs (#43325)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 11:36:34 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 334e280489 chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /superset-frontend (#43326)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 11:36:30 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c07f3ebf2d chore(deps-dev): bump @swc/plugin-emotion from 14.15.0 to 14.19.0 in /superset-frontend (#43328)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 11:36:26 -07:00
Sumit KumarandClaude Opus 4.8 1569915096 feat(multi-value): array-typed column filters with two-tier operators (ClickHouse MVP) (#41279)
Signed-off-by: thedeceptio <thedeceptio@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 10:56:49 -07:00
BexultanandBexultan Mustafin fde0ba26d1 fix(mcp): validate virtual dataset metadata and surface errors (#43129)
Co-authored-by: Bexultan Mustafin <bexultan.mustafin@ffins.kz>
2026-08-19 10:50:37 -07:00
Enzo Martellucci 540f8cb2d0 fix(sqllab): invalidate the dataset-save gate when the editor SQL changes 2026-08-19 16:52:40 +02:00
Enzo Martellucci aae997e546 fix(sql_lab): return 400 not 500 when raise_for_access hits malformed Jinja 2026-08-19 16:02:09 +02:00
DanielSwift1992 097c99b19c fix: remove a labeler glob that matches no files (#43270) 2026-08-18 16:21:01 -07:00
David Dallakyan 5ce52e531d fix(clickhouse): add PT1S time grain (#43217) 2026-08-18 15:49:18 -07:00
34cd50cc48 test(chart): mock the event log endpoint in the drill-to-detail menu test (#43183)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-18 13:51:54 -07:00
c0ab5f3385 fix(dashboard): preserve native filter keys for dataset-less filters on save (#42898)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
2026-08-18 21:30:50 +03:00
ʈᵃᵢ 7d4f30574f feat(tooltip): add Truncate labels control to timeseries charts (#43272) 2026-08-18 11:05:07 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e4ea6e23d8 chore(deps): bump supercluster from 8.0.1 to 9.0.0 in /superset-frontend (#43290)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 10:35:17 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a13a5f1af4 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.65.0 to 8.67.0 in /superset-websocket (#43284)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 10:34:39 -07:00
Endi Monan f994602096 fix(dashboard): align list OpenAPI schema (#43256) 2026-08-18 10:22:50 -07:00
Amin GhadersohiandClaude Fable 5 086b4af65d feat(mcp): per-resource token scopes with user-permission intersection (#42297)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 13:11:27 -04:00
fd063d17bf fix(security): harden account password-change and session-invalidation handling (#42934)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-18 17:40:08 +01:00
60e1802c52 fix(dashboard): mute the Group By display control loading spinner (#42879)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-18 09:37:43 -07:00
Joe LiandClaude Opus 4.8 2d1daac11a fix(explore): samples endpoint now honors requested row limit (#43148)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 09:28:04 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3c90bdc6f0 chore(deps-dev): bump oxfmt from 0.62.0 to 0.63.0 in /superset-frontend (#43293)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 06:15:08 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>rusackasClaude Opus 4.8
ebab31adc2 chore(deps): bump antd from 6.5.4 to 6.6.0 in /superset-frontend (#43294)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 06:15:04 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 936f073b9a chore(deps-dev): bump @typescript-eslint/parser from 8.66.0 to 8.67.0 in /superset-websocket (#43281)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 03:00:46 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d12320239a chore(deps-dev): bump typescript-eslint from 8.66.0 to 8.67.0 in /superset-websocket (#43282)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 03:00:42 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8ef7be5788 chore(deps-dev): bump oxfmt from 0.62.0 to 0.63.0 in /superset-websocket (#43283)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 03:00:39 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 72458ab26f chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.66.0 to 8.67.0 in /superset-frontend in the typescript-eslint group (#43285)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 03:00:33 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5105f13726 chore(deps-dev): bump the typescript-eslint group in /docs with 3 updates (#43286)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 03:00:29 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> bdafb6c330 chore(deps-dev): bump oxfmt from 0.62.0 to 0.63.0 in /docs (#43287)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 03:00:25 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b4d79462ec chore(deps-dev): bump oxlint from 1.77.0 to 1.78.0 in /superset-frontend (#43288)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 03:00:21 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 49374f1fe5 chore(deps): bump antd from 6.5.4 to 6.6.0 in /docs (#43289)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 03:00:17 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f766de6d0d chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /superset-frontend (#43291)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 02:59:59 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ed20e729d0 chore(deps): bump google-auth-library from 11.0.0 to 11.0.1 in /superset-frontend (#43292)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 02:59:55 -07:00
13eb47a1da fix(api): improved request handling and embedded dashboard scoping (#42930)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-08-17 23:50:25 -07:00
Elizabeth ThompsonandClaude Opus 4.8 98136d547c fix(reports): fail closed on alert screenshot capture instead of delivering a blank (#43031)
Signed-off-by: Elizabeth Thompson <eschutho@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 00:03:15 -04:00
Elizabeth ThompsonandClaude Opus 4.8 e2070d79dc fix(reports): wait for ECharts paint before capturing report screenshots (#43077)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 00:02:36 -04:00
ʈᵃᵢ 2807f1b0e8 fix(dashboard): report the real error when saving a dashboard fails (#43140) 2026-08-17 17:51:28 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c9c230142b chore(deps): bump baseline-browser-mapping from 2.11.12 to 2.11.13 in /docs (#43243)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 17:46:24 -07:00
Beto Dealmeida 2f8875aaef feat(semantic layers): optional metadata for metrics/dimensions (#43269) 2026-08-17 17:34:17 -07:00
Elizabeth Thompson 6e270df4a2 fix(sql-lab): catch TemplateError in StreamingSqlResultExportCommand.validate (#43226) 2026-08-17 15:02:11 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 97eafd6140 chore(deps-dev): bump @types/node from 26.1.2 to 26.2.0 in /superset-websocket (#43240)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:34:35 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3ed97f9691 chore(deps): bump ws from 8.21.2 to 8.21.3 in /superset-websocket (#43241)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:34:31 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5105899810 chore(deps-dev): bump eslint from 10.8.0 to 10.8.1 in /superset-websocket (#43242)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:34:25 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d917071708 chore(deps): bump caniuse-lite from 1.0.30001807 to 1.0.30001809 in /docs (#43244)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:34:18 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> afde126d9a chore(deps-dev): bump @testing-library/jest-dom from 7.0.0 to 7.0.1 in /superset-frontend (#43246)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:34:15 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b3a9b9beb4 chore(deps-dev): bump @types/node from 26.1.2 to 26.2.0 in /superset-frontend (#43247)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:34:10 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 70ba9c9552 chore(deps-dev): bump eslint from 10.8.0 to 10.8.1 in /superset-frontend (#43248)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:34:06 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 98276cd1f3 chore(deps-dev): bump baseline-browser-mapping from 2.11.12 to 2.11.13 in /superset-frontend (#43249)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:34:03 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> cdeca0c179 chore(deps-dev): bump tsx from 4.23.10 to 4.23.12 in /superset-frontend (#43250)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:33:59 -07:00
Amin Ghadersohi aaf9eba161 fix(db_engine_specs): skip malformed third-party dialect entry points (#43110)
Signed-off-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-08-17 14:10:06 -07:00
Mehmet Salih Yavuz 1991e3f0d2 fix(datasets): keep metric certified_by when certification details are typed next (#43255) 2026-08-17 19:23:31 +03:00
Mehmet Salih Yavuz cfd40bdd0d fix(dashboard): derive filter scope on read instead of serving a stale cache (#43252) 2026-08-17 19:23:19 +03:00
Endi Monan a8216e3787 fix(calendar): localize date labels (#43163) 2026-08-17 09:44:14 -03:00
anopsandEvan Rusackas d114eb638b fix(datasets): preserve metric warning_markdown when extra is absent (#42763)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-16 00:22:24 -07:00
Abdrihman Hussein 70d06e3d77 docs(faq): convert flask fab freeze question to a proper heading (#43221) 2026-08-15 23:21:13 +07:00
Joe Li a2c004266b fix(ci): update vulnerable transitive nanoid (#43166) 2026-08-15 23:09:19 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Đỗ Trọng Hải
2773bc94eb chore(deps-dev): bump sqlalchemy-bigquery from 1.17.1 to 1.17.2 (#43209)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-08-15 00:59:55 -07:00
94459ae885 ci: declare top-level permissions on the remaining workflows (#43197)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-08-15 14:48:43 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ad3103fdb0 chore(deps-dev): bump fastmcp from 3.4.5 to 3.4.7 (#43210)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 00:43:03 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
065342f8c9 chore(deps-dev): bump pandas-gbq from 0.35.0 to 0.35.1 (#43212)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 00:42:19 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2e8a8031f8 chore(deps): bump cachetools from 7.1.6 to 7.1.7 (#43208)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 14:33:36 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> cece082eed chore(deps-dev): update pyocient requirement from <4,>=1.0.15 to >=3.9.0,<4 (#43211)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-15 14:33:21 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
22dde07a8e chore(deps): bump mako from 1.3.12 to 1.4.1 (#43207)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 14:33:06 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe LiEvan Rusackas
93cc520482 chore(deps): bump @googleapis/sheets from 13.0.2 to 14.0.0 in /superset-frontend (#43047)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-15 14:23:13 +07:00
7679c5641a fix(lint-metrics): record oxlint plugin rules under their canonical id (#43198)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 14:21:38 +07:00
Evan RusackasandClaude Code f7c70568c3 fix(charts): fix Bubble chart crash and correct example's time_range to a year with real data (#43201)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-15 14:16:29 +07:00
Michael GerberandEvan Rusackas 69c4011a95 feat(embedded): Add setDataMask API method (#35754)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-14 22:07:24 -07:00
4684851336 fix(import): isolate per-tag import in a SAVEPOINT to avoid poisoned session (#42912) (#42920)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-14 21:56:43 -07:00
e7139a7ac2 fix(models): enforce one OAuth2 token per user+database (#42897)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 21:50:56 -07:00
488 changed files with 14712 additions and 4760 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
name: Bug report
description: Report a bug to improve Superset's stability
labels: ["bug"]
labels: ["#bug"]
body:
- type: markdown
attributes:
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: Cosmetic Issue
about: Describe a cosmetic issue with CSS, positioning, layout, labeling, or similar
labels: "cosmetic-issue"
labels: "#bug:cosmetic"
---
## Screenshot
+1 -1
View File
@@ -29,7 +29,7 @@
"dependencies:python":
- changed-files:
- any-glob-to-any-file:
- 'superset/requirements/**'
- 'requirements/**'
- 'superset/translations/requirements.txt'
- 'RELEASING/requirements.txt'
+4 -1
View File
@@ -21,6 +21,9 @@ on:
#schedule:
# - cron: '0 0 * * *' # Runs daily at midnight UTC
permissions:
contents: read
jobs:
bump-python-package:
runs-on: ubuntu-slim
@@ -45,7 +48,7 @@ jobs:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: "3.11"
enable-cache: true
@@ -16,6 +16,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
check_db_migration_conflict:
name: Check DB migration conflict
+5 -2
View File
@@ -14,6 +14,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
changes:
runs-on: ubuntu-26.04
@@ -64,7 +67,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -75,6 +78,6 @@ jobs:
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
category: "/language:${{matrix.language}}"
@@ -33,6 +33,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
enforce-single-migration-head:
runs-on: ubuntu-26.04
+3
View File
@@ -7,6 +7,9 @@ on:
pull_request:
types: [opened, edited]
permissions:
contents: read
jobs:
superbot-orglabel:
runs-on: ubuntu-slim
+3
View File
@@ -7,6 +7,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
labeler:
permissions:
+3
View File
@@ -3,6 +3,9 @@ on:
release:
types: [published] # This makes it run only when a new released is published
permissions:
contents: read
jobs:
latest-release:
name: Add/update tag to new release
+3
View File
@@ -19,6 +19,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: false
permissions:
contents: read
jobs:
lint-check:
runs-on: ubuntu-slim
+3
View File
@@ -17,6 +17,9 @@ env:
GITHUB_ORG: ${{ github.repository_owner }}
GITHUB_REPO: ${{ github.event.repository.name }}
permissions:
contents: read
jobs:
cleanup-expired:
name: Clean up expired showtime environments
+3
View File
@@ -26,6 +26,9 @@ env:
GITHUB_REPO: ${{ github.event.repository.name }}
GITHUB_ACTOR: ${{ github.actor }}
permissions:
contents: read
jobs:
sync:
name: 🎪 Sync PR to desired state
+3
View File
@@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
changes:
runs-on: ubuntu-26.04
@@ -27,6 +27,9 @@ concurrency:
group: helm-release
cancel-in-progress: false
permissions:
contents: read
jobs:
release:
runs-on: ubuntu-26.04
@@ -22,6 +22,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
changes:
runs-on: ubuntu-26.04
@@ -14,6 +14,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
changes:
runs-on: ubuntu-26.04
+3
View File
@@ -13,6 +13,9 @@ on:
required: true
type: string
permissions:
contents: read
jobs:
supersetbot:
runs-on: ubuntu-26.04
+3
View File
@@ -5,6 +5,9 @@ on:
pull_request_target:
types: [opened]
permissions:
contents: read
jobs:
welcome:
runs-on: ubuntu-slim
+26
View File
@@ -24,6 +24,31 @@ assists people when migrating to a new version.
## Next
- `SAMPLES_ROW_LIMIT` is now the default for `/datasource/samples` requests without a valid explicit `per_page`, rather than a hard per-request ceiling; explicit limits are honored up to the existing global row-limit ceiling, matching `/chart/data` SAMPLES requests.
### MCP tool results preserve stored string values
Structured MCP tool results no longer add `<UNTRUSTED-CONTENT>` wrappers or
rewrite delimiter-looking text inside string fields. Tool-result content remains
user-controlled data, but clients must convey that trust boundary outside domain
values instead of recognizing or removing marker strings.
Clients that handled the former delimiter convention should stop stripping marker
text: the same text can be legitimate stored content. Response models and content
types are unchanged, and no metadata-database migration is required. Automated
read-modify-write workflows should be paused or pinned away from older instances
until every serving instance is upgraded; a mixed-version response has no reliable
signal that tells a client whether its text is decorated. Redis-backed MCP response
caches use a new internal namespace after the upgrade, so upgraded instances do not
reuse older cached results.
Values that a client already wrote back with presentation wrappers cannot be
distinguished safely from intentional content. Operators should review possible
`<UNTRUSTED-CONTENT>` / `</UNTRUSTED-CONTENT>` wrappers and
`[ESCAPED-UNTRUSTED-CONTENT-OPEN]` /
`[ESCAPED-UNTRUSTED-CONTENT-CLOSE]` substitutions rather than applying an automatic
marker-removal migration.
### OAuth2 database callback metrics include their outcome
The unqualified `DatabaseRestApi.oauth2` StatsD counter has been replaced with
@@ -31,6 +56,7 @@ The unqualified `DatabaseRestApi.oauth2` StatsD counter has been replaced with
`DatabaseRestApi.oauth2.error`. Update monitoring rules and dashboards that consume
the old counter to use the outcome-specific replacements.
- [42930](https://github.com/apache/superset/pull/42930): Dataset import data-URI fetches no longer honor an HTTP(S) proxy when `DATASET_IMPORT_ALLOW_INTERNAL_DATA_URLS` is `False` (the default): the connection is now made directly to the destination so the peer-address check validates the real target instead of a proxy's. Deployments that require an egress proxy to reach legitimate external data URLs for dataset import should set `DATASET_IMPORT_ALLOW_INTERNAL_DATA_URLS = True` or otherwise ensure those URLs resolve without one.
- [42935](https://github.com/apache/superset/pull/42935): The MCP service now refuses to start (`MCPAuthConfigError`) when `MCP_JWT_ISSUER` trusts more than one issuer and no `MCP_USER_RESOLVER` is configured, instead of only logging a warning. This was already a documented misconfiguration (the default resolver isn't issuer-scoped, so distinct trusted issuers minting the same username/email would resolve to the same Superset user); deployments trusting multiple issuers must configure an `MCP_USER_RESOLVER` that derives its identity from the token's `iss` claim before upgrading. Single-issuer deployments are unaffected.
- [42393](https://github.com/apache/superset/pull/42393): Exported dataset YAML now carries a `uuid` for each metric and column so that custom folder assignments (which reference metrics/columns by UUID) survive an import into another workspace. This affects any export bundle that contains datasets, not just a dataset export: chart, dashboard, database and full-asset exports all embed the same dataset YAML, so a dashboard exported from this release also fails to import into an older one even though no dataset was exported directly. As with `folders` and `currency_code_column`, the affected `datasets/` files fail schema validation (`Unknown field: uuid`) when imported into Superset releases that predate this change; regenerate or hand-edit exports for older targets in mixed-version fleets.
- [42300](https://github.com/apache/superset/pull/42300): Timeseries charts (line/area/bar) with a Y-axis bound in effect — either an explicit `yAxisBounds` or one derived from `truncateYAxis` — now clamp out-of-range data points to that bound instead of letting ECharts drop the point (and the line segments around it) entirely. Any existing chart with a configured Y-axis bound and data outside it will look different after upgrading: a gap becomes a point pinned to the boundary. The clamp also rewrites the value ECharts reads for that point's tooltip and data label, so the displayed value is the bound rather than the true observation.
@@ -97,6 +97,54 @@ for more information on how to configure it.
At the very least, you'll want to change `SECRET_KEY` and `SQLALCHEMY_DATABASE_URI`. Continue reading for more about each of these.
## Localizing D3 date and time labels
`BABEL_DEFAULT_LOCALE` controls Superset's application translations, while
`D3_TIME_FORMAT` provides localized date and time names to visualizations that
use the D3 formatter registry, including Calendar Heatmap. Configure both when
you want the application and chart labels to use the same locale.
`D3_TIME_FORMAT` accepts partial overrides. For example, Russian month names
can be configured in `superset_config.py` as follows:
```python
BABEL_DEFAULT_LOCALE = "ru"
D3_TIME_FORMAT = {
"months": [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
],
"shortMonths": [
"Янв",
"Фев",
"Мар",
"Апр",
"Май",
"Июн",
"Июл",
"Авг",
"Сен",
"Окт",
"Ноя",
"Дек",
],
}
```
Restart Superset after changing `superset_config.py` so the frontend receives
the updated formatter configuration.
## Chart-data query timing
Set `CHART_DATA_INCLUDE_TIMING = True` to add an optional versioned timing object
+34 -2
View File
@@ -576,7 +576,7 @@ MCP_CACHE_CONFIG = {
| Key | Default | Description |
| -------------------- | --------- | ----------------------------------------------------------- |
| `enabled` | `False` | Enable response caching |
| `CACHE_KEY_PREFIX` | `None` | Optional prefix for cache keys (useful for shared Redis) |
| `CACHE_KEY_PREFIX` | `None` | Base prefix for shared Redis; Superset appends an internal response-contract namespace |
| `list_tools_ttl` | `300` | Cache TTL in seconds for `tools/list` |
| `list_resources_ttl` | `300` | Cache TTL for `resources/list` |
| `list_prompts_ttl` | `300` | Cache TTL for `prompts/list` |
@@ -718,6 +718,34 @@ Every MCP request passes through a middleware stack before reaching the tool fun
Additional middleware classes (`RateLimitMiddleware`, `FieldPermissionsMiddleware`, `PrivateToolMiddleware`) are implemented in `superset/mcp_service/middleware.py` but are not added to the default pipeline. They are available for operators who want to layer them in via a custom startup path.
### Tool Result Value Contract
Structured tool results preserve Superset domain values exactly. In particular,
string fields are not wrapped in trust delimiters, and text that resembles a
delimiter is returned as literal application data. This lets clients safely use a
read result as the basis for an update without persisting presentation markup.
All tool-result content should still be treated as user-controlled data with no
instruction authority. MCP clients should communicate that trust boundary through
their model instructions or presentation layer, outside the returned field values;
fixed or generated marker strings inside a value are ambiguous and must not be used
as a trust signal.
For compatibility, clients that supported the former
`<UNTRUSTED-CONTENT>` convention should stop recognizing or stripping those strings.
The response schemas and content types have not changed. Because marker-looking text
can be legitimate application data, a client cannot reliably distinguish a legacy
decorated response from a clean one. Pause automated read-modify-write workflows, or
route them only to upgraded instances, until every serving instance is upgraded.
Redis-backed MCP response caches include an internal response-contract namespace, so
an upgraded instance does not reuse responses cached by an older release. Older
instances can still return legacy values while they remain in service. After the
upgrade, review previously written values for wrapper text and both
`[ESCAPED-UNTRUSTED-CONTENT-OPEN]` and
`[ESCAPED-UNTRUSTED-CONTENT-CLOSE]`; do not remove these strings automatically,
because they may be intentional content.
### Error Sanitization
The `GlobalErrorHandlerMiddleware` automatically redacts sensitive information from all error messages before they reach the LLM client. The following are replaced with generic messages:
@@ -752,7 +780,11 @@ For a 3-pod Kubernetes deployment with the defaults above, expect up to 3 × (5
Enable response caching for read-heavy workloads (dashboards/datasets that don't change frequently). With the in-memory backend (default when `MCP_STORE_CONFIG` is disabled), caching is per-process. Use Redis-backed caching for consistent cache hits across multiple pods:
```python
MCP_CACHE_CONFIG = {"enabled": True, "call_tool_ttl": 3600}
MCP_CACHE_CONFIG = {
"enabled": True,
"CACHE_KEY_PREFIX": "mcp_cache_",
"call_tool_ttl": 3600,
}
MCP_STORE_CONFIG = {"enabled": True, "CACHE_REDIS_URL": "redis://redis:6379/0"}
```
+13 -1
View File
@@ -400,7 +400,7 @@ Once enabled, each user manages their own keys from their profile page:
1. Open the user menu (top-right) and click **Info** to navigate to the User Info page
2. Expand the **API Keys** section
3. Click **+ API Key**
4. Enter a name and (optionally) an expiration date
4. Enter a name and optionally select resource scopes
5. Copy the generated token — it is shown only once
Only users with the `can_read` and `can_write` permissions on `ApiKey` (granted by default to Admins) can manage API keys.
@@ -415,6 +415,18 @@ Authorization: Bearer <your-api-key>
This works for all REST API endpoints and the MCP server. The request is executed with the permissions of the user who created the key.
#### API Key Scopes
The creation dialog can restrict an API key to MCP resource actions such as
`superset:dashboard:read` or `superset:chart:write`. A scope is an additional
restriction: it never grants a permission that the creating user does not
already have through Superset RBAC. Write scopes also cover update and delete
operations for that resource; `superset:sqllab:write` covers SQL execution.
Keys created without scopes retain legacy RBAC-only behavior. The scoped-key
restrictions described here are enforced by the MCP server; regular REST API
routes continue to apply their existing Superset RBAC checks.
#### Use Cases
- **CI/CD pipelines** — automated chart/dashboard exports and imports
+49 -5
View File
@@ -46,13 +46,43 @@ import FAQSchema from '@site/src/components/FAQSchema';
answer:
'You need to register a free account at Mapbox.com, obtain an API key, and add it to your .env file at the key MAPBOX_API_KEY.',
},
{
question: 'How to limit the timed refresh on a dashboard?',
answer:
'To exclude specific slices from the timed refresh process, add the timed_refresh_immune_slices key to the dashboard JSON Metadata field with the slice IDs to exclude.',
},
{
question: "Why does 'flask fab' or Superset freeze, hang, or not respond when started (my home directory is NFS mounted)?",
answer:
'By default, Superset creates and uses an SQLite database at ~/.superset/superset.db. SQLite is known to not work well if used on NFS due to broken file locking. Override the path with the SUPERSET_HOME environment variable or change SQLALCHEMY_DATABASE_URI in superset_config.py.',
},
{
question: 'What if the table schema changed?',
answer:
'Go to Data -> Datasets, click the edit icon next to the dataset whose schema has changed, and hit Sync columns from source from the Columns tab. The new columns will get merged.',
},
{
question: 'What database engine can I use as a backend for Superset?',
answer:
'Superset is tested using MySQL, PostgreSQL, and SQLite backends for storing its internal metadata. While Superset supports many databases as data sources, only these are recommended for the metadata store in production.',
},
{
question: 'Does Superset work with my database?',
question: 'How can I configure OAuth authentication and authorization?',
answer:
'Take a look at the Flask-AppBuilder OAuth configuration example, which shows how to configure OAuth authentication and authorization in Superset.',
},
{
question: 'Is there a way to force the dashboard to use specific colors?',
answer:
'It is possible on a per-dashboard basis by providing a mapping of labels to colors in the JSON Metadata attribute using the label_colors key. You can use a full hex color, a named color, or the index in the current color palette.',
},
{
question: 'How do I expand all chart descriptions on a dashboard by default?',
answer:
'Add the expand_all_slices key to the dashboard JSON Metadata field with a value of true. Charts that have already been manually expanded or collapsed keep that per-chart override regardless of the setting.',
},
{
question: 'Does Superset work with [insert database engine here]?',
answer:
'Superset supports any database with a Python SQLAlchemy dialect and DBAPI driver. Check the Connecting to Databases documentation for the full list of supported databases.',
},
@@ -61,15 +91,30 @@ import FAQSchema from '@site/src/components/FAQSchema';
answer:
'Yes, Superset has a public REST API documented using Swagger. Enable FAB_API_SWAGGER_UI in superset_config.py to access interactive API documentation at /swagger/v1.',
},
{
question: 'How can I see usage statistics (e.g., monthly active users)?',
answer:
'This functionality is not included with Superset, but you can extract and analyze the logs table in Superset\u2019s metadata database to see what actions have occurred.',
},
{
question: 'What does Hours Offset in the Edit Dataset view do?',
answer:
'In the Edit Dataset view, the hours offset lets you configure the number of hours to be added or subtracted from the time column. This can be used, for example, to convert UTC time to local time.',
},
{
question: 'Does Superset collect any telemetry data?',
answer:
'Superset uses Scarf by default to collect basic telemetry data to help maintainers understand version usage. Users can opt out by setting the SCARF_ANALYTICS environment variable to false.',
},
{
question: 'Does Superset have a trash bin to recover deleted assets?',
question: 'Does Superset have an archive panel or trash bin from which a user can recover deleted assets?',
answer:
'No, there is no built-in way to recover deleted dashboards, charts, or datasets. It is recommended to take periodic backups of the metadata database and use export functionality for recovery.',
'No. Currently, there is no way to recover a deleted Superset dashboard, chart, dataset, or database from the UI. It is recommended to take periodic backups of the metadata database and use export functionality for recovery.',
},
{
question: 'I ran a security scan of the Superset container image and it showed dozens of "high" and "critical" vulnerabilities! Can you release a version of Superset without these?',
answer:
'These are dependency CVEs in software that Superset uses, mostly in the Linux kernel or Python. Superset addresses them by regularly updating dependencies and welcomes pull requests that fix dependency CVEs. The Superset security team focuses primarily on vulnerabilities in Superset itself.',
},
]}
/>
@@ -214,8 +259,7 @@ frontend falls back to a built-in default of `5000` milliseconds.
SUPERSET_DASHBOARD_MANUAL_REFRESH_STAGGER_MS = 0
```
**Why does flask fab or superset freeze/hang/not responding when started (my home directory is
NFS mounted)?**
## Why does `flask fab` or Superset freeze, hang, or not respond when started (my home directory is NFS mounted)?
By default, Superset creates and uses an SQLite database at `~/.superset/superset.db`. SQLite is
known to [not work well if used on NFS](https://www.sqlite.org/lockingv3.html) due to broken file
+13 -13
View File
@@ -58,14 +58,14 @@
"@fontsource/inter": "^5.3.0",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@storybook/addon-docs": "^10.5.7",
"@storybook/addon-docs": "^10.5.8",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.47",
"antd": "^6.5.4",
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001807",
"docusaurus-plugin-openapi-docs": "^5.1.3",
"docusaurus-theme-openapi-docs": "^5.1.3",
"antd": "^6.6.0",
"baseline-browser-mapping": "^2.11.13",
"caniuse-lite": "^1.0.30001809",
"docusaurus-plugin-openapi-docs": "^5.2.0",
"docusaurus-theme-openapi-docs": "^5.2.0",
"js-yaml": "^5.2.3",
"json-bigint": "^1.0.0",
"prism-react-renderer": "^2.4.1",
@@ -77,8 +77,8 @@
"react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.2.0",
"storybook": "^10.5.7",
"swagger-ui-react": "^5.32.12",
"storybook": "^10.5.8",
"swagger-ui-react": "^5.32.13",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",
"unist-util-visit": "^5.1.0"
@@ -89,14 +89,14 @@
"@eslint/js": "^9.39.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.66.0",
"@typescript-eslint/parser": "^8.66.0",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.9.0",
"oxfmt": "^0.62.0",
"globals": "^17.11.0",
"oxfmt": "^0.63.0",
"typescript": "~6.0.3",
"typescript-eslint": "^8.66.0",
"typescript-eslint": "^8.67.0",
"webpack": "^5.109.2"
},
"browserslist": {
+44 -32
View File
@@ -3407,22 +3407,26 @@
"nullable": true,
"type": "string"
},
"description": {
"nullable": true,
"type": "string"
},
"editors": {
"items": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.Subject"
},
"type": "array"
},
"id": {
"type": "integer"
},
"is_managed_externally": {
"type": "boolean"
},
"owners": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.User2"
},
"published": {
"nullable": true,
"type": "boolean"
},
"roles": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.Role"
},
"slug": {
"maxLength": 255,
"nullable": true,
@@ -3432,10 +3436,10 @@
"readOnly": true
},
"tags": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.Tag"
},
"thumbnail_url": {
"readOnly": true
"items": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.Tag"
},
"type": "array"
},
"url": {
"readOnly": true
@@ -3444,21 +3448,46 @@
"format": "uuid",
"nullable": true,
"type": "string"
},
"viewers": {
"items": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.Subject1"
},
"type": "array"
}
},
"type": "object"
},
"DashboardRestApi.get_list.Role": {
"DashboardRestApi.get_list.Subject": {
"properties": {
"id": {
"type": "integer"
},
"name": {
"maxLength": 64,
"label": {
"maxLength": 255,
"type": "string"
},
"type": {
"type": "integer"
}
},
"required": ["name"],
"required": ["label", "type"],
"type": "object"
},
"DashboardRestApi.get_list.Subject1": {
"properties": {
"id": {
"type": "integer"
},
"label": {
"maxLength": 255,
"type": "string"
},
"type": {
"type": "integer"
}
},
"required": ["label", "type"],
"type": "object"
},
"DashboardRestApi.get_list.Tag": {
@@ -3511,23 +3540,6 @@
"required": ["first_name", "last_name"],
"type": "object"
},
"DashboardRestApi.get_list.User2": {
"properties": {
"first_name": {
"maxLength": 64,
"type": "string"
},
"id": {
"type": "integer"
},
"last_name": {
"maxLength": 64,
"type": "string"
}
},
"required": ["first_name", "last_name"],
"type": "object"
},
"DashboardRestApi.post": {
"properties": {
"certification_details": {
@@ -16506,7 +16518,7 @@
},
"result": {
"items": {
"type": "object"
"$ref": "#/components/schemas/DashboardRestApi.get_list"
},
"type": "array"
}
+303 -285
View File
@@ -1142,6 +1142,11 @@
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768"
integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==
"@babel/runtime@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-8.0.0.tgz#d7bd513e6843662346552c2798ab895716cf97f2"
integrity sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==
"@babel/template@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700"
@@ -3170,100 +3175,100 @@
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.23.0.tgz#8b66dbfa7b796139e719063fc0e44084e80a1c15"
integrity sha512-gUGJpr+Rn6zMxm5juApV0K3U845i8t47o8k+rbO0BHbi4PoJIfSPeQmrE2dgohQm2g5k6iviNFyXCGqvmaYUpw==
"@oxfmt/binding-android-arm-eabi@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.62.0.tgz#3f5b9d3ba944f42ad3fa2697b9fef88a8c9d4ce0"
integrity sha512-pdsv0C4gPjJ8H1+sd8u0BDx+yLACTL+rgeMIOL1ln4ihSnhw8CWXtYWgvcSkyTfgGBIzFKab+d8rx9Xl4en/Kw==
"@oxfmt/binding-android-arm-eabi@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.63.0.tgz#136176dc94fdc41e21415cc770d86f5066282e0f"
integrity sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A==
"@oxfmt/binding-android-arm64@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.62.0.tgz#4c7e2c567f645ed051be100318e9e3f716630c1b"
integrity sha512-WC3YQ7uS/KtDrjmqwBviwFKe9qeoi+eXx8aX1z/ffG23Md75myjrJaQqTuJvdOLPoa4EYTjDWH0dHXfwulCVog==
"@oxfmt/binding-android-arm64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.63.0.tgz#10bc42457179210061c801122a64304619e3bdab"
integrity sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==
"@oxfmt/binding-darwin-arm64@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.62.0.tgz#6c8007ae65ed17f9d1ecc6c680da19ec19276c67"
integrity sha512-GM8Yf3LjjaR1I8PD0SfeoIlwhsh9GvSF+cQ8sf624Yxnjsyumn95aFzYfKJVefblfDIiOAnZ7QVm2sa21Er/0Q==
"@oxfmt/binding-darwin-arm64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.63.0.tgz#5f9084d9a760a1836387f8970a7f9d614ec3d909"
integrity sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==
"@oxfmt/binding-darwin-x64@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.62.0.tgz#0661a0274e8625921c5a054aeb21a36251946e6b"
integrity sha512-d5THp7F8bCxLqNogEXDORRsQD6dosf3EyFtnXfBer6v+8tGdcWIjoDX9WaXrrF/26zOmL8qHpPTKCEvpBDmZkQ==
"@oxfmt/binding-darwin-x64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.63.0.tgz#badd4a02218a9a62319817d5c337b30159a54a21"
integrity sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==
"@oxfmt/binding-freebsd-x64@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.62.0.tgz#f3345001102ac3e6c2947920d6d1676e9cf97e75"
integrity sha512-1DnrtXGZooOZ0fHgAXZUaDQzBVh1CM2MNW4oBXyQ2aWKvCHjyljvT9fgBkOM0fEOb96X5eqtcfJ0YUVt9jj66g==
"@oxfmt/binding-freebsd-x64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.63.0.tgz#a17261e95c8ebef1f76d8aaac746a64fdb6ba51e"
integrity sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==
"@oxfmt/binding-linux-arm-gnueabihf@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.62.0.tgz#ddc03bc2a899f2071d6706c06dfdec3a7f3e8b5a"
integrity sha512-4pQDHOYRH+Huqe0StIaWyvk2CVl/aTaqSrbZpA3/pLS2xH24ME7lBgYprhQF2fRkHBzhGGGKliwxFsDdHwx59g==
"@oxfmt/binding-linux-arm-gnueabihf@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.63.0.tgz#baeee34bb08e0769af878623f442e83bc0aacd7a"
integrity sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==
"@oxfmt/binding-linux-arm-musleabihf@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.62.0.tgz#5e82208d612c4caf64ada75e129e34d1a9eefb2c"
integrity sha512-X0jAaZJFMCVKhB6YyWVTQ/wN2DLsBcZKSMqTS76bF6riT+XZdtg2FPEdjDvdVbunO9cG+tWiVaEs4Zs38lxYog==
"@oxfmt/binding-linux-arm-musleabihf@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.63.0.tgz#e70d5697ec4b6bb5f87a3f019e01b3f956b8e44b"
integrity sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==
"@oxfmt/binding-linux-arm64-gnu@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.62.0.tgz#eb379bc58aa962e753d58b4cc68ff4081bc19a5d"
integrity sha512-682Z8T5s8T5ATArYtsejKvbIfd8LEAXyyDkKkoZVq8HND7Vx8TYLlrDjDSeYfodMeVwHOgkj13lJYR8cj6vUSg==
"@oxfmt/binding-linux-arm64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.63.0.tgz#638a8ed4f3d256c50aeb6d2c19cfc65792c902e1"
integrity sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==
"@oxfmt/binding-linux-arm64-musl@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.62.0.tgz#dc1c62510405e874bf6a53a34f548032eb6dfed7"
integrity sha512-lk25fAl7KWaLWVJcW0CHEXB7QlQZtx5eDkjpaGMK0hzXTjUe0Wmlu8IKuFHoviSOcEJedRTs4VE/506VqGxGew==
"@oxfmt/binding-linux-arm64-musl@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.63.0.tgz#af5a9b787f5233f27a3360ad56235fc1b011f760"
integrity sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==
"@oxfmt/binding-linux-ppc64-gnu@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.62.0.tgz#9f9afee327090024db86b70ec81a57ad06bb2f00"
integrity sha512-SFyNqHQLwySceWNLhiSldx7wPXRAzP0L0WcW9GegP3uWrpZGJiZlQO85NbHAFPEfxR9PhZ9qSnZryEh7+v+4Gw==
"@oxfmt/binding-linux-ppc64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.63.0.tgz#c1a211206134a5577e355a495989e0d733218d60"
integrity sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==
"@oxfmt/binding-linux-riscv64-gnu@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.62.0.tgz#4427d42ee3bad0e55b38dc76fe14a7e5318c360c"
integrity sha512-KYj55C1ywJfHo6+aKDuEmUtVEdJALsC5GwayDGsI6FGz2GxFqNr/mA8nxVsNbJzm7sE5MRqTQ9ziImSzhYXysA==
"@oxfmt/binding-linux-riscv64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.63.0.tgz#4863f0311e5c1b88f75ef822959b3ca4fd938937"
integrity sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==
"@oxfmt/binding-linux-riscv64-musl@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.62.0.tgz#a117f82909f075cf07c333842d89a5638429e21d"
integrity sha512-BhZDNo5GOU5nC378RhD0/XpvaEBHsH3HLgJp8YZX3A0InC7oivzA63HsRmiXFLtLSHAstEVrDf6fbC7Rs8Jh/A==
"@oxfmt/binding-linux-riscv64-musl@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.63.0.tgz#ad05a017d12553e2f544743c4940adb552aa1d1c"
integrity sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==
"@oxfmt/binding-linux-s390x-gnu@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.62.0.tgz#3fac79fefe7ffc3f0a9393678ebd782aac918fcd"
integrity sha512-UyAFmyHkgSgUJ/wOM4p3U8AC2yAFvRH5PNBs7TnK0fObTT/XSWcdr/lAzPSWaekHaZFaMeFZyk9n93Joq3J93A==
"@oxfmt/binding-linux-s390x-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.63.0.tgz#2803f539db15bc66db115888fa8f84d6531ed2b9"
integrity sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==
"@oxfmt/binding-linux-x64-gnu@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.62.0.tgz#8da207bef27941f0265c129d1c7c82c7cf91d1ce"
integrity sha512-1iYMP0leytWazFubD/WnINJuIrzRPuoL1aWEJdlGezEzDbTxcd29R4r8IUzP2oWeKst5V02uMJgR2NILlPlG6w==
"@oxfmt/binding-linux-x64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.63.0.tgz#c22a06a60ae2d6b3de522095e0c50a816040a033"
integrity sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==
"@oxfmt/binding-linux-x64-musl@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.62.0.tgz#e55cf9b7c8c2204fdbb5d4818f8c5ba02aa49360"
integrity sha512-4rA/URtJSTVNVAQz6Q8wf7SaRvOXVy+TizriT9hs/Y1XhLR/R+92uWKRQG8yFWRAIEBbFHJ6WevQcl/G9SXEfw==
"@oxfmt/binding-linux-x64-musl@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.63.0.tgz#48d3eeaf8e3757f638cf92de5ee4858befc9c0a3"
integrity sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==
"@oxfmt/binding-openharmony-arm64@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.62.0.tgz#4998769ee1b5894efcd6cb99729d5a75f4c09dd1"
integrity sha512-mSZuFHU2ar1KLUjXpI2QBQcJ1VsOB3mOCgQXuXCpKs19dgh4u+OaovNfrWDfiJb+ihJ2+f7YFcaO9bS2dlTCXA==
"@oxfmt/binding-openharmony-arm64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.63.0.tgz#02be9e140ae35ba30f52bdce27612fece4a01ab3"
integrity sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==
"@oxfmt/binding-win32-arm64-msvc@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.62.0.tgz#e09eaabdde76c885c4f8a190518c2eb2de08548a"
integrity sha512-OfwuhkcjDlqC4EgDojtiV9mzpLqeB9KqTOWPOjLEYBVdDCVSxqW3qzp/xcIxsbtI0UgGCnKvAqYKyY25kf5JZw==
"@oxfmt/binding-win32-arm64-msvc@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.63.0.tgz#2226eaf52b6345a2cb926499216b2486cf0dbec2"
integrity sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==
"@oxfmt/binding-win32-ia32-msvc@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.62.0.tgz#f36e306308923977365270d8b26290f4ca2fcfa5"
integrity sha512-P9uDDNFRzghO3X8QAzhkjKhK7JvtABsVn8UYtFX7uor12IAnwNt8nNIctvfWj1JkQU/kE+fmLRPiw7XlrIHsZw==
"@oxfmt/binding-win32-ia32-msvc@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.63.0.tgz#58d263bb5ecd7330c02f9dcd8cda10f66e42e74b"
integrity sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==
"@oxfmt/binding-win32-x64-msvc@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.62.0.tgz#bb6545e581d5ee7111084dbabeec7fe548bae418"
integrity sha512-dlI5SY7XYQCiCBafntWagCR6HcAJB/NpsLtdlPx8x08+Osz8Ok1HHz1GZuusegCe/VoJ6pAnF5a4pd5OZAq7qQ==
"@oxfmt/binding-win32-x64-msvc@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.63.0.tgz#02a166c8a8049c55d0096d1ba9d8e73f3a4d26a7"
integrity sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==
"@parcel/watcher-android-arm64@2.5.6":
version "2.5.6"
@@ -3532,13 +3537,13 @@
dependencies:
"@babel/runtime" "^7.24.4"
"@rc-component/cascader@~1.17.0":
version "1.17.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.17.0.tgz#52c0eceada2c7b4b37ebe822c19a6544b9562edf"
integrity sha512-3cVNG0zrQF1PoXq262L3wGCU+/YLEC1mGSVHDl577dQmA0ZKkXFbY6nwyXo+beCcM7buo49t24jkr+QZdL7O8w==
"@rc-component/cascader@~1.22.0":
version "1.22.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.22.0.tgz#eec0b6f4d2df5903aa12cfed321a578705926937"
integrity sha512-SffrA57aS9oub3VuI7ajPhJTPtaNxngSvtRhD40Rd8dwJ5vfWPSrVanWgeepdWFGBt7EHftIK5RUU0u3rCTwWw==
dependencies:
"@rc-component/select" "~1.8.0"
"@rc-component/tree" "~1.3.2"
"@rc-component/select" "~1.10.0"
"@rc-component/tree" "~1.4.0"
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
@@ -3614,14 +3619,14 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/image@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/image/-/image-1.9.0.tgz#110785d735d20336afcdbac84e8fbfd059a7a44e"
integrity sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ==
"@rc-component/image@~1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@rc-component/image/-/image-1.10.0.tgz#5d7a82d20e4c91f75875ea64eb1dadd7af676b1d"
integrity sha512-BjeZCRQ+hw+4WAhvrw8rJvy5fckA2xpf/X2XQEOABUHvLTNB9inB98X3Mp54jYQ7g10DfWERQWHXeC4ylxp1Uw==
dependencies:
"@rc-component/motion" "^1.0.0"
"@rc-component/portal" "^2.1.2"
"@rc-component/util" "^1.10.1"
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/input-number@~1.6.2":
@@ -3633,7 +3638,7 @@
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/input@~1.3.0", "@rc-component/input@~1.3.1":
"@rc-component/input@~1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@rc-component/input/-/input-1.3.1.tgz#230b8b59cdde8521d50f0eede63ddacb61cc0cd3"
integrity sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig==
@@ -3642,15 +3647,27 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/mentions@~1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@rc-component/mentions/-/mentions-1.10.0.tgz#46b1117cfb0c716b476e97f342555eccc2f41c97"
integrity sha512-CI1njYUVY0NjHtLhNoVmXlJyy568Sfep9Wsak6vmGjtT6uazx98djGYlCXz2xkHhEm73g91Y3MTvzUyE5avI7w==
"@rc-component/listy@~1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@rc-component/listy/-/listy-1.2.3.tgz#e9c8ef4f409c231b44dded37e63ae2875bbe0334"
integrity sha512-IXiMjV5s0rczLBlfh7G5nB4M3365mrEeedjwKtf5I+Ns3PqRUsebR2h5u8CeFarsVfLUPC2I5p0h09TNoOWyvQ==
dependencies:
"@rc-component/input" "~1.3.0"
"@rc-component/motion" "^1.1.4"
"@rc-component/portal" "^2.0.0"
"@rc-component/resize-observer" "^1.0.0"
"@rc-component/util" "^1.3.1"
"@rc-component/virtual-list" "^1.4.0"
clsx "^2.1.1"
"@rc-component/mentions@~1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@rc-component/mentions/-/mentions-1.11.0.tgz#cee0c4710f26766ad8550d386cfec5ff86fd58d9"
integrity sha512-IC2qXuEBMFHxPIXEFfYWj6Sr7UiDZnOqJHCYQBbwPzopBJOPZIR6mV9U4QH1bYQRlKYlYnIsajWDMgVGgWQyWQ==
dependencies:
"@rc-component/input" "~1.3.1"
"@rc-component/menu" "~1.4.0"
"@rc-component/trigger" "^3.0.0"
"@rc-component/util" "^1.3.0"
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/menu@~1.4.0", "@rc-component/menu@~1.4.1":
@@ -3724,7 +3741,7 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/portal@^2.1.0", "@rc-component/portal@^2.1.2", "@rc-component/portal@^2.1.3", "@rc-component/portal@^2.2.0", "@rc-component/portal@^2.2.1":
"@rc-component/portal@^2.0.0", "@rc-component/portal@^2.1.0", "@rc-component/portal@^2.1.2", "@rc-component/portal@^2.1.3", "@rc-component/portal@^2.2.0", "@rc-component/portal@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@rc-component/portal/-/portal-2.2.1.tgz#37c34b4c8cd73f53cc7072c96dd0e9ac332669ec"
integrity sha512-ck+r1kW/JSv0wxPji3KN2ss9K6Z0qqwusw/mf/0JobXhZ8hC2ejZwCJObW/SvDi0uhA0VzmCnx0CaCci95tcmA==
@@ -3772,10 +3789,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/select@~1.8.0", "@rc-component/select@~1.8.2":
version "1.8.2"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.8.2.tgz#f016992dae5c57186535512d73783e2fc7e4c59e"
integrity sha512-HQ9zuYqjfZTlcEMWlU1GAPBajd2OHIMVHyjZSGVTCVARwkfCgvXZMTEn0cduy3L+ejAKkaZluOQvxovZoaJaQw==
"@rc-component/select@~1.10.0":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.10.1.tgz#323b2f458a637e8e752f8341094783741c613c34"
integrity sha512-H+yQsl+qED9NilQ3g6zdpsMwUgwVjrcMTkNHAWRVU/MoNCYgTbDgU+MIMgZDK+rVdd2JUfI/MkysMcZZ0cyQKw==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/trigger" "^3.0.0"
@@ -3807,10 +3824,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/table@~1.10.4":
version "1.10.4"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.10.4.tgz#8c4e33bc150aa39f579c15426421348a789de326"
integrity sha512-HwoTnrwc29zeoXkXGhWqzJh8FIibGUxi1jM4LtoSzmR9d5Vv5osUQpZxnXKBP8iOCvyD6BQzZm1nXJRcnrxpAg==
"@rc-component/table@~1.11.0":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.11.1.tgz#7b5c2a7c26fd37b6a403082029b5a72fcb330a4d"
integrity sha512-OWdS6DMmeWb7bJBGqPxYZpQbzBlBiXZUu2sqo6Ii7Sjs9GeK1IsrXrWk26SL2c6KEseabswdxrRj7WUm9LdECw==
dependencies:
"@rc-component/context" "^2.0.1"
"@rc-component/resize-observer" "^1.0.0"
@@ -3818,10 +3835,10 @@
"@rc-component/virtual-list" "^1.0.1"
clsx "^2.1.1"
"@rc-component/tabs@~1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@rc-component/tabs/-/tabs-1.11.0.tgz#c157b2fadcdc2f3ab6c69d0098f73e03c6aa0c12"
integrity sha512-hA/drZYOVa/MMIb4M2fWf3yaTyTG4qVuIABmghvEhyfw2nBob5VTH69lMCDjSVKmgODjO6nWlCV+gVn3xBrj5Q==
"@rc-component/tabs@~1.12.0":
version "1.12.0"
resolved "https://registry.yarnpkg.com/@rc-component/tabs/-/tabs-1.12.0.tgz#41a1a77ed1afc4f1b8b727003a058c631aceea1b"
integrity sha512-XL7Kqy5fnUE2WTlO1/fCGrrfNlGFebdr7JseGkEIjzcVMAtIFQJ8sqCSOmxcXstjU6fonD/4rnhZHxj7sDTajQ==
dependencies:
"@rc-component/dropdown" "~1.0.0"
"@rc-component/menu" "~1.4.0"
@@ -3830,13 +3847,13 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/tooltip@~1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@rc-component/tooltip/-/tooltip-1.4.0.tgz#c8cf15c6773218a5a36271467f06e663f99c28e7"
integrity sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==
"@rc-component/tooltip@~1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@rc-component/tooltip/-/tooltip-1.5.0.tgz#422aa0760b310e0a1d0f9f7223e7f0d455de57a2"
integrity sha512-agQ/+mBqrEQfTX4D3KhQ7j+ZbX4/VHjoJ7Noa2wIdZ1/FbQTOd7Sn92rp+jtCoqAVTLUgSOydePIgZ204gi2EQ==
dependencies:
"@rc-component/trigger" "^3.7.1"
"@rc-component/util" "^1.3.0"
"@rc-component/trigger" "^3.10.0"
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/tour@~2.4.0":
@@ -3849,27 +3866,27 @@
"@rc-component/util" "^1.7.0"
clsx "^2.1.1"
"@rc-component/tree-select@~1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.11.0.tgz#9080cdf1d28f2ddd6d8a4879b7aa90d3170f7db9"
integrity sha512-EhS0X0wtUhBfK4S5TlpSY3MR9ndPMGgujtt1PJW3Ej+ToAlnS/6ohYURtCoXBYGqazUwHmgQGVUDsfpVwhWPkg==
"@rc-component/tree-select@~1.16.0":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.16.1.tgz#dcaea96e396e98108cb29cc051840d4fbdda38cc"
integrity sha512-a1Oi6EJhqAhdOxxupdJi6fP0RPHMKn5TcfkX2+llaQ4lF4nwfH7b6SCHcnsybaa2s+pk1yZYwVyeOYkDnEBRdg==
dependencies:
"@rc-component/select" "~1.8.0"
"@rc-component/tree" "~1.3.2"
"@rc-component/select" "~1.10.0"
"@rc-component/tree" "~1.4.0"
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/tree@~1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.3.2.tgz#4b0c13564314eff61ca948c18ef923b87c9d7e44"
integrity sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA==
"@rc-component/tree@~1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.4.0.tgz#c0031180e681389bf0bdcb867a0087525b45c8a9"
integrity sha512-dGsJGDJQedA0BqqVgj3F8BvHXTSZijyhTXdbAdkcx8lynzZkty/CV3Z3LOm/fxz+BCfl3dfGiAQpb7Q5XNvl0Q==
dependencies:
"@rc-component/motion" "^1.0.0"
"@rc-component/util" "^1.11.1"
"@rc-component/virtual-list" "^1.2.0"
clsx "^2.1.1"
"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.10.1", "@rc-component/trigger@^3.6.15", "@rc-component/trigger@^3.7.1":
"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.10.0", "@rc-component/trigger@^3.10.1", "@rc-component/trigger@^3.6.15":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-3.10.1.tgz#cb28e1bc0745a2af6897dd7ec774f9b56dc88f86"
integrity sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw==
@@ -3888,7 +3905,7 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/util@^1.10.1", "@rc-component/util@^1.11.0", "@rc-component/util@^1.11.1", "@rc-component/util@^1.12.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.7.0", "@rc-component/util@^1.9.0":
"@rc-component/util@^1.11.0", "@rc-component/util@^1.11.1", "@rc-component/util@^1.12.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.3.1", "@rc-component/util@^1.4.0", "@rc-component/util@^1.7.0", "@rc-component/util@^1.9.0":
version "1.12.0"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.12.0.tgz#58e453585810bcb8a35ff1aafd5e01187457b86f"
integrity sha512-AEjPL8JVdohIITaiXokyjL9WQ6tKWWjAYK9QU16tGNE9JaQABBQy+hA4H2Lup5MgXy9yY3iLrbZJheuU13hTdQ==
@@ -3906,6 +3923,16 @@
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/virtual-list@^1.4.0":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@rc-component/virtual-list/-/virtual-list-1.5.1.tgz#71c5844a8d6bd5b3501dfb66419d3a4612b2bb18"
integrity sha512-boqHxdtyWC88u8quYgEO49bcBy5fzRiOcnBge+N4nLzs2k8hUQ/yw7JE9dM6yCBE4jSm5YSHVCVMS+suBuJGKA==
dependencies:
"@babel/runtime" "^8.0.0"
"@rc-component/resize-observer" "^1.0.1"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@redocly/ajv@^8.18.1":
version "8.18.3"
resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.18.3.tgz#a925753d9a33375219f1b2ba91aef320f9929577"
@@ -4095,23 +4122,23 @@
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
"@storybook/addon-docs@^10.5.7":
version "10.5.7"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.7.tgz#6d599c94fc871c248ce06a5c081f57655c83f40a"
integrity sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA==
"@storybook/addon-docs@^10.5.8":
version "10.5.8"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.8.tgz#767c10c7a4cc1b625b93f869b2a2b09fc8514f2e"
integrity sha512-NlHiMKW/UvW/uL8HXFDCEVwoH3qZeGYZ/qlWax4d7H471b/T54MBq2KcB4ZrdA785FfIH3numAJdBb5jwn00Mg==
dependencies:
"@mdx-js/react" "^3.0.0"
"@storybook/csf-plugin" "10.5.7"
"@storybook/csf-plugin" "10.5.8"
"@storybook/icons" "^2.0.2"
"@storybook/react-dom-shim" "10.5.7"
"@storybook/react-dom-shim" "10.5.8"
react "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
react-dom "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
ts-dedent "^2.0.0"
"@storybook/csf-plugin@10.5.7":
version "10.5.7"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.7.tgz#bc73f164d1b5f8e2931b2774f4b389a06453cf6e"
integrity sha512-IaX8FlM0H36HNFhJ2+4L9bCldqfvHGqcLg841SJNyK/DhfMlM7JsvY/GDH2ZFuWrUf8FSOx96GRRnHq6XfRKag==
"@storybook/csf-plugin@10.5.8":
version "10.5.8"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.8.tgz#c626c5bfe55d0e279b2457e5cf150a788d1fc637"
integrity sha512-/FHiMyOWWEXfwK/lM0WxmkP9GLzbSJJuzGtfeuNWSOVDnvAMbjavitxfHb5wSbWKIQo0XYC1EJ2Y7x91XNYP4w==
dependencies:
unplugin "^2.3.5"
@@ -4125,10 +4152,10 @@
resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-2.1.0.tgz#edfc2450a39c5e780f28c6cbc49acd7bff59b41a"
integrity sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==
"@storybook/react-dom-shim@10.5.7":
version "10.5.7"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.7.tgz#9a5aa0e0f89c09e71c6cbfc6bb1abeb537e5aabf"
integrity sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA==
"@storybook/react-dom-shim@10.5.8":
version "10.5.8"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.8.tgz#40cc3e32af424baa2e4109a325dae2ede29999e2"
integrity sha512-N8D13/Xny+V3kfe1KBgsAHS0nKWXLLdgOOXS9poKdYzVwVCN+CGEGBxWX0zMMtdCptqa6/57em9coPlZMoO+bg==
"@superset-ui/core@^0.20.4":
version "0.20.4"
@@ -5658,110 +5685,100 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.66.0", "@typescript-eslint/eslint-plugin@^8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz#76e86aa5a2459fbf5bbd7a839c0dc0cce1d56224"
integrity sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==
"@typescript-eslint/eslint-plugin@8.67.0", "@typescript-eslint/eslint-plugin@^8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz#52f9f0e47d5a7571c4336e69bfeea581509ef2cf"
integrity sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.66.0"
"@typescript-eslint/type-utils" "8.66.0"
"@typescript-eslint/utils" "8.66.0"
"@typescript-eslint/visitor-keys" "8.66.0"
"@typescript-eslint/scope-manager" "8.67.0"
"@typescript-eslint/type-utils" "8.67.0"
"@typescript-eslint/utils" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.66.0", "@typescript-eslint/parser@^8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.66.0.tgz#88e3865ecf73b0118134e7cb831da87a961a57a1"
integrity sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==
"@typescript-eslint/parser@8.67.0", "@typescript-eslint/parser@^8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.67.0.tgz#0158022ec9927e0afcd58a8cc2ad57e01d892f5c"
integrity sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==
dependencies:
"@typescript-eslint/scope-manager" "8.66.0"
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/typescript-estree" "8.66.0"
"@typescript-eslint/visitor-keys" "8.66.0"
"@typescript-eslint/scope-manager" "8.67.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
debug "^4.4.3"
"@typescript-eslint/project-service@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.66.0.tgz#828f788895df52d9eb2b543445a3a5a13e35ab4e"
integrity sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==
"@typescript-eslint/project-service@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.67.0.tgz#1552db007ca9206a1c6c7acf49e210bd17a8c56f"
integrity sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.66.0"
"@typescript-eslint/types" "^8.66.0"
"@typescript-eslint/tsconfig-utils" "^8.67.0"
"@typescript-eslint/types" "^8.67.0"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz#4fffcc6ebd0df9fe7983c0256967567ea6f5ac63"
integrity sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==
"@typescript-eslint/scope-manager@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz#4d4c2da09560d10dd7d947cba2d29d14d25af16d"
integrity sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==
dependencies:
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/visitor-keys" "8.66.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
"@typescript-eslint/tsconfig-utils@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz#3a89066c507aa30541dc176804685b4b444e1e52"
integrity sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==
"@typescript-eslint/tsconfig-utils@^8.66.0":
"@typescript-eslint/tsconfig-utils@8.67.0", "@typescript-eslint/tsconfig-utils@^8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz#f45a3eba6b9132fb47141ec03ce2f275f1ea991d"
integrity sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==
"@typescript-eslint/type-utils@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz#b2315303eca72fad9afa7be4f58f053c8f2a0479"
integrity sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==
"@typescript-eslint/type-utils@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz#96bed105275559df3bcf0449b73a6414d35c59ce"
integrity sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==
dependencies:
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/typescript-estree" "8.66.0"
"@typescript-eslint/utils" "8.66.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/utils" "8.67.0"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.66.0.tgz#3cacab94d3b564c1d48c56eb37b89f89a6d48479"
integrity sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==
"@typescript-eslint/types@^8.66.0":
"@typescript-eslint/types@8.67.0", "@typescript-eslint/types@^8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.67.0.tgz#4a8d00cc1faba5c14feabc60f85b7a32652f34b6"
integrity sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==
"@typescript-eslint/typescript-estree@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz#1a38c3a97dc6c669b66d585d7f90ebc4fbb32a50"
integrity sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==
"@typescript-eslint/typescript-estree@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz#116c3a47c06119c5a050e8851861d6497dd64bc2"
integrity sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==
dependencies:
"@typescript-eslint/project-service" "8.66.0"
"@typescript-eslint/tsconfig-utils" "8.66.0"
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/visitor-keys" "8.66.0"
"@typescript-eslint/project-service" "8.67.0"
"@typescript-eslint/tsconfig-utils" "8.67.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.66.0.tgz#e277d67427043cdca2580ee91aa62921e4689969"
integrity sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==
"@typescript-eslint/utils@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.67.0.tgz#3e478a3d69d330a1fc50c12746cc2ee0732ccfcd"
integrity sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.66.0"
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/typescript-estree" "8.66.0"
"@typescript-eslint/scope-manager" "8.67.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/visitor-keys@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz#4c494e94745fb2724a4f37a310091e56b644d18a"
integrity sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==
"@typescript-eslint/visitor-keys@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz#601d40af9acf82a28da2286f3edafc69bba9017f"
integrity sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==
dependencies:
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/types" "8.67.0"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -6164,10 +6181,10 @@ ansis@^3.2.0:
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
antd@^6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.5.4.tgz#b41665e86a5f46ca761abd3b0abef7460116ca0d"
integrity sha512-jchA6i0rEwHjLpgC+l6HeLHP0gL4Q4yjs6Mxqt6PlhGD5ArxCj3ZH+fKFbNquCtd6Rlzzi+emfNFpP2dGLwZzg==
antd@^6.6.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.6.0.tgz#8acb84c54b36594b5c1a9084c8acb6a03b79961b"
integrity sha512-UDwWIbpmrCHB9ZQ+bPh4vQfB6DTI2ulIyoQ0Tc9xxalFblttiNGHl3ySBD9SyV/8+gUjFzfSx1+iU1Fog2i46w==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.1.2"
@@ -6176,7 +6193,7 @@ antd@^6.5.4:
"@ant-design/icons" "^6.3.2"
"@ant-design/react-slick" "~2.0.0"
"@babel/runtime" "^7.29.2"
"@rc-component/cascader" "~1.17.0"
"@rc-component/cascader" "~1.22.0"
"@rc-component/checkbox" "~2.0.0"
"@rc-component/collapse" "~1.2.0"
"@rc-component/color-picker" "~3.1.1"
@@ -6184,10 +6201,11 @@ antd@^6.5.4:
"@rc-component/drawer" "~1.4.2"
"@rc-component/dropdown" "~1.0.3"
"@rc-component/form" "~1.8.6"
"@rc-component/image" "~1.9.0"
"@rc-component/image" "~1.10.0"
"@rc-component/input" "~1.3.1"
"@rc-component/input-number" "~1.6.2"
"@rc-component/mentions" "~1.10.0"
"@rc-component/listy" "~1.2.3"
"@rc-component/mentions" "~1.11.0"
"@rc-component/menu" "~1.4.1"
"@rc-component/motion" "^1.3.3"
"@rc-component/mutate-observer" "^2.0.1"
@@ -6199,16 +6217,16 @@ antd@^6.5.4:
"@rc-component/rate" "~1.0.1"
"@rc-component/resize-observer" "^1.1.2"
"@rc-component/segmented" "~1.3.0"
"@rc-component/select" "~1.8.2"
"@rc-component/select" "~1.10.0"
"@rc-component/slider" "~1.1.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
"@rc-component/table" "~1.10.4"
"@rc-component/tabs" "~1.11.0"
"@rc-component/tooltip" "~1.4.0"
"@rc-component/table" "~1.11.0"
"@rc-component/tabs" "~1.12.0"
"@rc-component/tooltip" "~1.5.0"
"@rc-component/tour" "~2.4.0"
"@rc-component/tree" "~1.3.2"
"@rc-component/tree-select" "~1.11.0"
"@rc-component/tree" "~1.4.0"
"@rc-component/tree-select" "~1.16.0"
"@rc-component/trigger" "^3.10.1"
"@rc-component/upload" "~1.1.1"
"@rc-component/util" "^1.12.0"
@@ -6504,10 +6522,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.12, baseline-browser-mapping@^2.9.19:
version "2.11.12"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz#42ac48770bf73d292f60ce8ba4dc5e7ebb242ec3"
integrity sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.13, baseline-browser-mapping@^2.9.19:
version "2.11.13"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz#660073103c1bee93e54df55f117b7528adf6af19"
integrity sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==
batch@0.6.1:
version "0.6.1"
@@ -6745,10 +6763,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001807:
version "1.0.30001807"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001807.tgz#a113854941fb45b4c1f51793f4636920489079b4"
integrity sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001809:
version "1.0.30001809"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz#e6cf71f14ddfe008f114dd2a846923be3c03a07b"
integrity sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==
ccount@^2.0.0:
version "2.0.1"
@@ -7996,10 +8014,10 @@ doctrine@^2.1.0:
dependencies:
esutils "^2.0.2"
docusaurus-plugin-openapi-docs@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-5.1.3.tgz#b8cd5f8451aaf881deb1a744a8295685f1681865"
integrity sha512-HnpblSBdXoR39VNTIW9zWERUsMJxXOpvdQoBKyaTkUBPwCM48Z76+ndo2yO2vADq+EhWjJlfxL1DUzCrgNjThQ==
docusaurus-plugin-openapi-docs@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-5.2.0.tgz#8318ec90cd21fed023be57696211af7d72fd81db"
integrity sha512-MjrfRAMB64uvdxRVz6L9AXWe4QFjCdoBAzYs306yyI3nnXHsFj2lv2FnLA90JV9CAUZaGiYMvvkzBo2Nrkq/9w==
dependencies:
"@apidevtools/json-schema-ref-parser" "^15.3.3"
"@redocly/openapi-core" "^2.25.2"
@@ -8017,10 +8035,10 @@ docusaurus-plugin-openapi-docs@^5.1.3:
swagger2openapi "^7.0.8"
xml-formatter "^3.6.6"
docusaurus-theme-openapi-docs@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-5.1.3.tgz#e23644a63785352abbc76e42760c0dfdff3669e1"
integrity sha512-npbD1QahtjAEmrOet/86i5fTmcJX4/rPhVT+c0qKjm7StUNbyqjwchSVBQuU1rB69T51JOA9TpT/y6QcB9Xjvw==
docusaurus-theme-openapi-docs@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-5.2.0.tgz#6d93a74e2e3cf0ae77d24e1c4144bd2e74a52115"
integrity sha512-L0b80LzaMUfr76a9EQXRPCf8nxkEz8Xo6Aknnke1UeE2oXsgoiVki6U+RTE7GmJRjO8zSNKXyckGmGmqqWuHeA==
dependencies:
"@hookform/error-message" "^2.0.1"
"@reduxjs/toolkit" "^2.8.2"
@@ -8105,7 +8123,7 @@ domhandler@^5.0.2, domhandler@^5.0.3:
dependencies:
domelementtype "^2.3.0"
dompurify@^3.3.3, dompurify@^3.4.12:
dompurify@^3.3.3, dompurify@^3.4.13:
version "3.4.13"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.13.tgz#fc28949d59f92d62e28a3a764bcbeee35897a1be"
integrity sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==
@@ -9156,10 +9174,10 @@ globals@^14.0.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globals@^17.9.0:
version "17.9.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.9.0.tgz#e43f252d6bbe71508da43902a1709c8895a59f70"
integrity sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==
globals@^17.11.0:
version "17.11.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.11.0.tgz#d643485bb30220d7751e511cf4f68c73d3870d87"
integrity sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==
globalthis@^1.0.4:
version "1.0.4"
@@ -10266,10 +10284,10 @@ js-levenshtein@^1.1.6:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@4.1.0, js-yaml@=4.3.0, js-yaml@^4.1.0, js-yaml@^4.1.1, js-yaml@^4.2.0, js-yaml@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.0.tgz#d1900572a7f7cf0b5f540c83673e60bad3436592"
integrity sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==
js-yaml@4.1.0, js-yaml@=4.3.1, js-yaml@^4.1.0, js-yaml@^4.1.1, js-yaml@^4.2.0, js-yaml@^4.3.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848"
integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==
dependencies:
argparse "^2.0.1"
@@ -12244,32 +12262,32 @@ oxc-resolver@^11.19.1:
"@oxc-resolver/binding-win32-arm64-msvc" "11.23.0"
"@oxc-resolver/binding-win32-x64-msvc" "11.23.0"
oxfmt@^0.62.0:
version "0.62.0"
resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.62.0.tgz#9945728022d26dc0a1d5bc486db112e7e340507a"
integrity sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ==
oxfmt@^0.63.0:
version "0.63.0"
resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.63.0.tgz#c7338e6c43a68d5cf8dc61c08b617d77cb54e323"
integrity sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw==
dependencies:
tinypool "2.1.0"
optionalDependencies:
"@oxfmt/binding-android-arm-eabi" "0.62.0"
"@oxfmt/binding-android-arm64" "0.62.0"
"@oxfmt/binding-darwin-arm64" "0.62.0"
"@oxfmt/binding-darwin-x64" "0.62.0"
"@oxfmt/binding-freebsd-x64" "0.62.0"
"@oxfmt/binding-linux-arm-gnueabihf" "0.62.0"
"@oxfmt/binding-linux-arm-musleabihf" "0.62.0"
"@oxfmt/binding-linux-arm64-gnu" "0.62.0"
"@oxfmt/binding-linux-arm64-musl" "0.62.0"
"@oxfmt/binding-linux-ppc64-gnu" "0.62.0"
"@oxfmt/binding-linux-riscv64-gnu" "0.62.0"
"@oxfmt/binding-linux-riscv64-musl" "0.62.0"
"@oxfmt/binding-linux-s390x-gnu" "0.62.0"
"@oxfmt/binding-linux-x64-gnu" "0.62.0"
"@oxfmt/binding-linux-x64-musl" "0.62.0"
"@oxfmt/binding-openharmony-arm64" "0.62.0"
"@oxfmt/binding-win32-arm64-msvc" "0.62.0"
"@oxfmt/binding-win32-ia32-msvc" "0.62.0"
"@oxfmt/binding-win32-x64-msvc" "0.62.0"
"@oxfmt/binding-android-arm-eabi" "0.63.0"
"@oxfmt/binding-android-arm64" "0.63.0"
"@oxfmt/binding-darwin-arm64" "0.63.0"
"@oxfmt/binding-darwin-x64" "0.63.0"
"@oxfmt/binding-freebsd-x64" "0.63.0"
"@oxfmt/binding-linux-arm-gnueabihf" "0.63.0"
"@oxfmt/binding-linux-arm-musleabihf" "0.63.0"
"@oxfmt/binding-linux-arm64-gnu" "0.63.0"
"@oxfmt/binding-linux-arm64-musl" "0.63.0"
"@oxfmt/binding-linux-ppc64-gnu" "0.63.0"
"@oxfmt/binding-linux-riscv64-gnu" "0.63.0"
"@oxfmt/binding-linux-riscv64-musl" "0.63.0"
"@oxfmt/binding-linux-s390x-gnu" "0.63.0"
"@oxfmt/binding-linux-x64-gnu" "0.63.0"
"@oxfmt/binding-linux-x64-musl" "0.63.0"
"@oxfmt/binding-openharmony-arm64" "0.63.0"
"@oxfmt/binding-win32-arm64-msvc" "0.63.0"
"@oxfmt/binding-win32-ia32-msvc" "0.63.0"
"@oxfmt/binding-win32-x64-msvc" "0.63.0"
p-cancelable@^3.0.0:
version "3.0.0"
@@ -14765,10 +14783,10 @@ stop-iteration-iterator@^1.1.0:
es-errors "^1.3.0"
internal-slot "^1.1.0"
storybook@^10.5.7:
version "10.5.7"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.7.tgz#adfc465e51f337291c095278c23f1b8024ef2da7"
integrity sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg==
storybook@^10.5.8:
version "10.5.8"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.8.tgz#d5f051983e6232c0a73ea02149a72c7bafb43275"
integrity sha512-rR4oFMSiWBSqI0lvsJPtcQUPj8+hzj3TkLu+Mw61Wo6YxPSb5FsLSHai0jZnuaIdKIlmu25KCfwlSQl4e1uvnA==
dependencies:
"@storybook/global" "^5.0.0"
"@storybook/icons" "^2.0.2"
@@ -15085,10 +15103,10 @@ swagger-client@^3.37.8:
"@swagger-api/apidom-parser-adapter-openapi-yaml-3-2" "^1.12.0"
"@swagger-api/apidom-parser-adapter-yaml-1-2" "^1.12.0"
swagger-ui-react@^5.32.12:
version "5.32.12"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.32.12.tgz#47525a26774eb02db0e6203af72f5b32fa6205cc"
integrity sha512-WCdkNOQyMTZDu+z356FpwVWHf1dwZgQPUjdQPh1L4r7jULaJTKKlIItXq6WsZdYeXvsHndMdxxccEQXOAroUHQ==
swagger-ui-react@^5.32.13:
version "5.32.13"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.32.13.tgz#04c96140b0a2d4ea01ebec4d4cfc655d5ed9a500"
integrity sha512-XIDl+Ny6kE1N8wpSPiOFrjPfAevs4GR4XmV6BT6NLMikkMFIbIVocWbA8pnKYyYXQe8Rccfli5o2zDfySw0FnQ==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"
@@ -15097,11 +15115,11 @@ swagger-ui-react@^5.32.12:
classnames "^2.5.1"
css.escape "1.5.1"
deep-extend "0.6.0"
dompurify "^3.4.12"
dompurify "^3.4.13"
ieee754 "^1.2.1"
immutable "^4.3.9"
js-file-download "^0.4.12"
js-yaml "=4.3.0"
js-yaml "=4.3.1"
lodash "^4.18.1"
prop-types "^15.8.1"
randexp "^0.5.3"
@@ -15467,15 +15485,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.66.0:
version "8.66.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.66.0.tgz#0809b6d25c8a0924690ba30dc1f05607093c11fb"
integrity sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==
typescript-eslint@^8.67.0:
version "8.67.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.67.0.tgz#1e92de09ee0ff2d96cc0848f5e9f345ea930d963"
integrity sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==
dependencies:
"@typescript-eslint/eslint-plugin" "8.66.0"
"@typescript-eslint/parser" "8.66.0"
"@typescript-eslint/typescript-estree" "8.66.0"
"@typescript-eslint/utils" "8.66.0"
"@typescript-eslint/eslint-plugin" "8.67.0"
"@typescript-eslint/parser" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/utils" "8.67.0"
typescript@~6.0.3:
version "6.0.3"
+6 -6
View File
@@ -42,7 +42,7 @@ dependencies = [
# ``google-auth`` 2.53+ dropped it, so Superset must declare it
# explicitly to keep fresh ``pip install apache-superset`` working
# without the ``base.txt`` lock file (#40962).
"cachetools>=7.1.6, <8",
"cachetools>=7.1.7, <8",
"celery>=5.6.3, <6.0.0",
"click>=8.4.2",
"click-option-group",
@@ -75,7 +75,7 @@ dependencies = [
"humanize",
"isodate",
"jsonpath-ng>=1.8.0, <2",
"Mako>=1.2.2",
"Mako>=1.4.1",
"markdown>=3.10.3",
# marshmallow 4 compatibility: see superset/marshmallow_compatibility.py for a
# Flask-AppBuilder workaround. Tracking issue:
@@ -135,10 +135,10 @@ athena = ["pyathena[pandas]>=3.35.4, <4"]
# superset/db_engine_specs/aurora.py's known_incompatibilities metadata.
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.35.0",
"pandas-gbq>=0.35.1",
# 1.17.1 is likely the final release: googleapis/python-bigquery-sqlalchemy
# was archived 2026-05-16. Both 1.17.0 and 1.17.1 support SQLAlchemy 1.4/2.0.
"sqlalchemy-bigquery>=1.17.1",
"sqlalchemy-bigquery>=1.17.2",
"google-cloud-bigquery>=3.42.3",
]
clickhouse = ["clickhouse-connect>=1.6.0, <2.0"]
@@ -185,7 +185,7 @@ excel = ["xlrd>=2.0.2, <2.1"]
# installing this extra is only required to actually run exports.
excel-export = ["boto3"]
fastmcp = [
"fastmcp>=3.4.5,<4.0",
"fastmcp>=3.4.6,<4.0",
# tiktoken backs the response-size-guard token estimator. Without
# it, the middleware falls back to a coarser character-based
# heuristic that under-counts JSON-heavy MCP responses.
@@ -221,7 +221,7 @@ ocient = [
# unpinned sqlalchemy>=1.4 declared, but SQLAlchemy 2.0 support is
# unverified. Lower confidence than the other bumps in this PR.
"sqlalchemy-ocient>=3.0.0, <4",
"pyocient>=1.0.15, <4",
"pyocient>=3.9.0, <4",
"shapely",
"geojson",
]
+1 -1
View File
@@ -28,7 +28,7 @@ numexpr>=2.9.0
# Security: CVE-2026-34073 (MEDIUM) - Improper Certificate Validation
cryptography>=50.0.0,<51.0.0
# Security: Snyk - XSS vulnerability in Mako templates
mako>=1.3.11,<2.0.0
mako>=1.4.1,<2.0.0
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers
pyarrow>=24.0.0,<26.0.0
# Security: CVE-2026-27459 - pyopenssl certificate validation
+2 -2
View File
@@ -44,7 +44,7 @@ cachelib==0.13.0
# via
# flask-caching
# flask-session
cachetools==7.1.6
cachetools==7.1.7
# via apache-superset (pyproject.toml)
cattrs==25.1.1
# via requests-cache
@@ -207,7 +207,7 @@ kombu==5.6.2
# via celery
limits==5.1.0
# via flask-limiter
mako==1.3.12
mako==1.4.1
# via
# -r requirements/base.in
# apache-superset (pyproject.toml)
+6 -6
View File
@@ -99,7 +99,7 @@ cachelib==0.13.0
# -c requirements/base-constraint.txt
# flask-caching
# flask-session
cachetools==7.1.6
cachetools==7.1.7
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -239,9 +239,9 @@ et-xmlfile==2.0.0
# openpyxl
exceptiongroup==1.3.0
# via fastmcp-slim
fastmcp==3.4.5
fastmcp==3.4.7
# via apache-superset
fastmcp-slim==3.4.5
fastmcp-slim==3.4.7
# via fastmcp
filelock==3.20.3
# via
@@ -510,7 +510,7 @@ limits==5.1.0
# flask-limiter
lz4==4.4.5
# via trino
mako==1.3.12
mako==1.4.1
# via
# -c requirements/base-constraint.txt
# alembic
@@ -640,7 +640,7 @@ pandas==2.3.3
# db-dtypes
# pandas-gbq
# prophet
pandas-gbq==0.35.0
pandas-gbq==0.35.1
# via apache-superset
parameterized==0.9.0
# via apache-superset
@@ -964,7 +964,7 @@ sqlalchemy==2.0.51
# sqlalchemy-bigquery
# sqlalchemy-continuum
# sqlalchemy-utils
sqlalchemy-bigquery==1.17.1
sqlalchemy-bigquery==1.17.2
# via apache-superset
sqlalchemy-continuum==1.7.0
# via
+1 -1
View File
@@ -93,7 +93,7 @@ def find_models(module: ModuleType) -> list[type[Model]]: # noqa: C901
# where the current model is out-of-sync with the existing table after a
# downgrade
sqlalchemy_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]
engine = create_engine(sqlalchemy_uri, future=True)
engine = create_engine(sqlalchemy_uri)
Base = automap_base() # noqa: N806
Base.prepare(engine, reflect=True)
seen = set()
@@ -18,8 +18,9 @@
from __future__ import annotations
import enum
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import date, datetime, time, timedelta
from typing import Any
import isodate
import pyarrow as pa
@@ -90,6 +91,8 @@ class Dimension:
definition: str | None = None
description: str | None = None
grain: Grain | None = None
verbose_name: str | None = field(default=None, compare=False)
metadata: dict[str, Any] = field(default_factory=dict, compare=False)
class AggregationType(str, enum.Enum):
@@ -121,6 +124,9 @@ class Metric:
definition: str
description: str | None = None
aggregation: AggregationType | None = None
verbose_name: str | None = field(default=None, compare=False)
d3format: str | None = field(default=None, compare=False)
metadata: dict[str, Any] = field(default_factory=dict, compare=False)
@dataclass(frozen=True)
@@ -0,0 +1,97 @@
# 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 pyarrow as pa
from superset_core.semantic_layers.types import Dimension, Metric
def test_dimension_metadata_is_not_part_of_identity() -> None:
first = Dimension(
"sales.region",
"region",
pa.utf8(),
verbose_name="Region",
metadata={"display_name": "Region"},
)
second = Dimension(
"sales.region",
"region",
pa.utf8(),
verbose_name="Sales region",
metadata={"display_name": "Sales region"},
)
assert first == second
assert {first, second} == {first}
def test_metric_metadata_is_not_part_of_identity() -> None:
first = Metric(
"sales.total_revenue",
"total_revenue",
pa.float64(),
"SUM(revenue)",
verbose_name="Total revenue",
d3format="$,.2f",
metadata={"unit": {"kind": "currency", "code": "USD"}},
)
second = Metric(
"sales.total_revenue",
"total_revenue",
pa.float64(),
"SUM(revenue)",
verbose_name="Revenue",
d3format=",.0f",
metadata={"unit": {"kind": "currency", "code": "EUR"}},
)
assert first == second
assert {first, second} == {first}
def test_metric_accepts_superset_presentation_fields() -> None:
metric = Metric(
"sales.total_revenue",
"total_revenue",
pa.float64(),
"SUM(revenue)",
verbose_name="Total revenue",
d3format="$,.2f",
)
assert metric.verbose_name == "Total revenue"
assert metric.d3format == "$,.2f"
def test_dimension_accepts_superset_presentation_fields() -> None:
dimension = Dimension(
"sales.region",
"region",
pa.utf8(),
verbose_name="Region",
)
assert dimension.verbose_name == "Region"
def test_metadata_defaults_are_not_shared() -> None:
first = Metric("first", "first", pa.int64(), "COUNT(*)")
second = Metric("second", "second", pa.int64(), "COUNT(*)")
first.metadata["display_name"] = "First"
assert second.metadata == {}
+506
View File
@@ -19,6 +19,7 @@
"@babel/preset-typescript": "7.29.7",
"@types/node": "^25.4.0",
"babel-loader": "^9.1.3",
"jsdom": "^26.1.0",
"tscw-config": "^1.1.2",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
@@ -26,6 +27,27 @@
"webpack-cli": "^5.1.4"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
}
},
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/@babel/cli": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.29.7.tgz",
@@ -1656,6 +1678,121 @@
"node": ">=6.9.0"
}
},
"node_modules/@csstools/color-helpers": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.1.0",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz",
@@ -2501,6 +2638,16 @@
"acorn": "^8.14.0"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@ -2868,6 +3015,34 @@
"node": ">= 8"
}
},
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -2886,6 +3061,13 @@
}
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2915,6 +3097,19 @@
"node": ">=10.13.0"
}
},
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/envinfo": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz",
@@ -3316,6 +3511,60 @@
"node": ">= 0.4"
}
},
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"whatwg-encoding": "^3.1.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/import-local": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
@@ -3438,6 +3687,13 @@
"node": ">=0.10.0"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3498,6 +3754,46 @@
"dev": true,
"license": "MIT"
},
"node_modules/jsdom": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.5.0",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.16",
"parse5": "^7.2.1",
"rrweb-cssom": "^0.8.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^5.1.1",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.1.1",
"ws": "^8.18.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -3977,6 +4273,13 @@
"node": ">=0.10.0"
}
},
"node_modules/nwsapi": {
"version": "2.2.24",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
"integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
"dev": true,
"license": "MIT"
},
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
@@ -4033,6 +4336,19 @@
"node": ">=6"
}
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4142,6 +4458,16 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -4314,6 +4640,33 @@
"@rolldown/binding-win32-x64-msvc": "1.1.3"
}
},
"node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"dev": true,
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT"
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"dev": true,
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/schema-utils": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
@@ -4447,6 +4800,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
@@ -4592,6 +4952,26 @@
"node": ">=14.0.0"
}
},
"node_modules/tldts": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"tldts-core": "^6.1.86"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
"dev": true,
"license": "MIT"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4605,6 +4985,32 @@
"node": ">=8.0"
}
},
"node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^6.1.32"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tscw-config": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/tscw-config/-/tscw-config-1.1.2.tgz",
@@ -4931,6 +5337,19 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/watchpack": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
@@ -4944,6 +5363,16 @@
"node": ">=10.13.0"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/webpack": {
"version": "5.105.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz",
@@ -5070,6 +5499,44 @@
"node": ">=10.13.0"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"dev": true,
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5114,6 +5581,45 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"node_modules/ws": {
"version": "8.21.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true,
"license": "MIT"
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+1
View File
@@ -43,6 +43,7 @@
"@babel/preset-typescript": "7.29.7",
"@types/node": "^25.4.0",
"babel-loader": "^9.1.3",
"jsdom": "^26.1.0",
"tscw-config": "^1.1.2",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
+213
View File
@@ -0,0 +1,213 @@
/**
* 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.
*/
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { Switchboard } from "@superset-ui/switchboard";
import { embedDashboard } from "./index";
vi.mock("@superset-ui/switchboard");
function makeFakeJWT(claims: any) {
// not a valid jwt, but close enough for this code
const tokenifiedClaims = Buffer.from(JSON.stringify(claims)).toString(
"base64",
);
return `abc.${tokenifiedClaims}.xyz`;
}
describe("embedDashboard", () => {
let mountPoint: HTMLElement;
let mockSwitchboard: Switchboard;
beforeEach(() => {
mountPoint = document.createElement("div");
document.body.appendChild(mountPoint);
mockSwitchboard = {
emit: vi.fn(),
get: vi.fn(),
start: vi.fn(),
defineMethod: vi.fn(),
} as any;
// Constructor mocks must use `function`, since arrow functions cannot be
// invoked with `new`.
vi.mocked(Switchboard).mockImplementation(function () {
return mockSwitchboard;
} as any);
// Mock MessageChannel API
globalThis.MessageChannel = vi.fn(function (this: any) {
this.port1 = {};
this.port2 = {};
}) as any;
// Mock iframe load event and sandbox
const originalCreateElement = document.createElement.bind(document);
vi.spyOn(document, "createElement").mockImplementation(tagName => {
const element = originalCreateElement(tagName);
if (tagName === "iframe") {
// Mock sandbox DOMTokenList
(element as any).sandbox = {
add: vi.fn(),
};
// Mock contentWindow for postMessage
Object.defineProperty(element, "contentWindow", {
writable: true,
value: {
postMessage: vi.fn(),
},
});
setTimeout(() => {
element.dispatchEvent(new Event("load"));
}, 0);
}
return element;
});
});
afterEach(() => {
document.body.removeChild(mountPoint);
vi.restoreAllMocks();
});
test("setDataMask sends dataMask to iframe", async () => {
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
const testDataMask = {
"NATIVE_FILTER-1": {
filterState: {
value: ["value1", "value2"],
},
},
};
const dashboard = await embedDashboard({
id: "test-id",
supersetDomain: "https://superset.example.com",
mountPoint,
fetchGuestToken: mockFetchGuestToken,
});
dashboard.setDataMask(testDataMask);
expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
dataMask: testDataMask,
});
});
test("setDataMask sends empty dataMask", async () => {
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
const emptyDataMask = {};
const dashboard = await embedDashboard({
id: "test-id",
supersetDomain: "https://superset.example.com",
mountPoint,
fetchGuestToken: mockFetchGuestToken,
});
dashboard.setDataMask(emptyDataMask);
expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
dataMask: emptyDataMask,
});
});
test("setDataMask drops the change-trigger flags observeDataMask adds", async () => {
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
const observedMask = {
"NATIVE_FILTER-1": {
filterState: {
value: ["CA"],
},
},
crossFiltersChanged: false,
nativeFiltersChanged: true,
};
const dashboard = await embedDashboard({
id: "test-id",
supersetDomain: "https://superset.example.com",
mountPoint,
fetchGuestToken: mockFetchGuestToken,
});
dashboard.setDataMask(observedMask);
expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
dataMask: {
"NATIVE_FILTER-1": observedMask["NATIVE_FILTER-1"],
},
});
});
test("setDataMask sends complex dataMask with multiple filters", async () => {
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
const complexDataMask = {
"NATIVE_FILTER-1": {
filterState: {
value: ["CA", "NY"],
},
},
"NATIVE_FILTER-2": {
filterState: {
value: [2023, 2024],
},
},
};
const dashboard = await embedDashboard({
id: "test-id",
supersetDomain: "https://superset.example.com",
mountPoint,
fetchGuestToken: mockFetchGuestToken,
});
dashboard.setDataMask(complexDataMask);
expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
dataMask: complexDataMask,
});
});
test("setDataMask rejects when the embedded page does not support it", async () => {
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
vi.mocked(mockSwitchboard.get).mockRejectedValue(
new Error('Method "setDataMask" is not defined'),
);
const dashboard = await embedDashboard({
id: "test-id",
supersetDomain: "https://superset.example.com",
mountPoint,
fetchGuestToken: mockFetchGuestToken,
});
await expect(dashboard.setDataMask({})).rejects.toThrow(
'Method "setDataMask" is not defined',
);
});
});
+22
View File
@@ -115,6 +115,12 @@ export type EmbeddedDashboard = {
getActiveTabs: () => Promise<string[]>;
observeDataMask: (callbackFn: ObserveDataMaskCallbackFn) => void;
getDataMask: () => Promise<Record<string, any>>;
/**
* Applies a data mask to the dashboard.
* Rejects if the embedded Superset page does not support `setDataMask`,
* so a version mismatch surfaces instead of silently doing nothing.
*/
setDataMask: (dataMask: Record<string, any>) => Promise<void>;
getChartStates: () => Promise<Record<string, any>>;
getChartDataPayloads: (params?: {
chartId?: number;
@@ -355,6 +361,21 @@ export async function embedDashboard({
ourPort.get<string>("getDashboardPermalink", { anchor });
const getActiveTabs = () => ourPort.get<string[]>("getActiveTabs");
const getDataMask = () => ourPort.get<Record<string, any>>("getDataMask");
// `observeDataMask` hands the host a mask with the change-trigger booleans
// mixed in, so feeding that payload straight back into `setDataMask` is a
// natural thing for a host to do. Keep only the entries that look like a
// filter's mask, so those flags never reach the dashboard as filter ids.
// Sent with `get` rather than `emit` so the iframe acknowledges the call:
// an embedded page that predates `setDataMask` replies with an error instead
// of dropping the message silently.
const setDataMask = (dataMask: Record<string, any>) =>
ourPort.get<void>("setDataMask", {
dataMask: Object.fromEntries(
Object.entries(dataMask).filter(
([, mask]) => typeof mask === "object" && mask !== null,
),
),
});
const getChartStates = () =>
ourPort.get<Record<string, any>>("getChartStates");
const getChartDataPayloads = (params?: { chartId?: number }) =>
@@ -396,6 +417,7 @@ export async function embedDashboard({
getActiveTabs,
observeDataMask,
getDataMask,
setDataMask,
getChartStates,
getChartDataPayloads,
setThemeConfig,
+423 -703
View File
File diff suppressed because it is too large Load Diff
+18 -17
View File
@@ -110,7 +110,7 @@
"@fontsource/fira-code": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/inter": "^5.3.0",
"@googleapis/sheets": "^13.0.2",
"@googleapis/sheets": "^14.0.0",
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
"@jsonforms/core": "^3.7.0",
"@jsonforms/react": "^3.7.0",
@@ -158,7 +158,7 @@
"@visx/xychart": "^4.0.0",
"ag-grid-community": "36.1.0",
"ag-grid-react": "36.1.0",
"antd": "^6.5.4",
"antd": "^6.6.0",
"chrono-node": "^2.10.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
@@ -176,7 +176,7 @@
"geostyler-openlayers-parser": "^5.7.1",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^11.0.0",
"google-auth-library": "^11.0.2",
"immer": "^11.1.16",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -257,16 +257,16 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.62.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.5.7",
"@storybook/addon-links": "10.5.7",
"@storybook/react-webpack5": "10.5.7",
"@storybook/addon-docs": "10.5.8",
"@storybook/addon-links": "10.5.8",
"@storybook/react-webpack5": "10.5.8",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.47",
"@swc/plugin-emotion": "^14.15.0",
"@swc/plugin-emotion": "^14.19.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
@@ -277,7 +277,7 @@
"@types/json-bigint": "^1.0.4",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
"@types/node": "^26.1.2",
"@types/node": "^26.2.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react-loadable": "^5.5.11",
@@ -289,19 +289,19 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.66.0",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.63.0",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"baseline-browser-mapping": "^2.11.12",
"baseline-browser-mapping": "^2.11.14",
"cheerio": "1.2.0",
"concurrently": "^10.0.4",
"copy-webpack-plugin": "^14.0.0",
"cross-env": "^10.1.0",
"css-loader": "^7.1.4",
"eslint": "^10.8.0",
"eslint": "^10.8.1",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
@@ -312,7 +312,7 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
"eslint-plugin-storybook": "10.5.7",
"eslint-plugin-storybook": "10.5.8",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -331,8 +331,8 @@
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.6.1",
"open-cli": "^9.0.0",
"oxfmt": "^0.62.0",
"oxlint": "^1.77.0",
"oxfmt": "^0.63.0",
"oxlint": "^1.78.0",
"po2json": "^0.4.5",
"postcss-styled-syntax": "^0.7.2",
"process": "^0.11.10",
@@ -343,13 +343,13 @@
"source-map": "^0.8.0",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.5.7",
"storybook": "10.5.8",
"style-loader": "^4.0.0",
"stylelint": "^17.14.1",
"swc-loader": "^0.2.7",
"ts-jest": "^29.4.12",
"tscw-config": "^1.1.2",
"tsx": "^4.23.10",
"tsx": "^4.23.12",
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"wait-on": "^9.1.0",
@@ -415,6 +415,7 @@
"minimatch@>=10": {
"brace-expansion": ">=5.0.8"
},
"nanoid@>=3 <4": "3.3.18",
"nwsapi": "^2.2.13",
"puppeteer": "^22.4.1",
"tar": "^7.5.16",
@@ -130,6 +130,7 @@ export enum GenericDataType {
String = 1,
Temporal = 2,
Boolean = 3,
MultiValue = 4,
}
/**
@@ -28,6 +28,7 @@ import {
FieldBinaryOutlined,
FieldStringOutlined,
NumberOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import { Icons } from '@superset-ui/core/components';
@@ -72,6 +73,10 @@ export function ColumnTypeLabel({ type }: ColumnTypeLabelProps) {
typeIcon = <FieldBinaryOutlined aria-label={t('boolean type icon')} />;
} else if (type === GenericDataType.Temporal) {
typeIcon = <ClockCircleOutlined aria-label={t('temporal type icon')} />;
} else if (type === GenericDataType.MultiValue) {
typeIcon = (
<UnorderedListOutlined aria-label={t('multi-value type icon')} />
);
}
return <TypeIconWrapper>{typeIcon}</TypeIconWrapper>;
@@ -64,4 +64,21 @@ describe('ColumnOption', () => {
renderColumnTypeLabel({ type: GenericDataType.Temporal });
expect(screen.getByLabelText('temporal type icon')).toBeVisible();
});
test('multi-value (array) type shows list icon', () => {
renderColumnTypeLabel({ type: GenericDataType.MultiValue });
expect(screen.getByLabelText('multi-value type icon')).toBeVisible();
});
});
describe('GenericDataType enum parity', () => {
// These numeric values are shared with the backend enum in
// superset/utils/core.py (GenericDataType). They must stay in sync because
// the backend serializes columns using these integers.
test('values match the backend contract', () => {
expect(GenericDataType.Numeric).toBe(0);
expect(GenericDataType.String).toBe(1);
expect(GenericDataType.Temporal).toBe(2);
expect(GenericDataType.Boolean).toBe(3);
expect(GenericDataType.MultiValue).toBe(4);
});
});
@@ -103,7 +103,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.1",
"@types/lodash": "^4.17.25",
"@types/node": "^26.1.2",
"@types/node": "^26.2.0",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -107,12 +107,14 @@ const getAllSelectOptions = () =>
const findSelectOption = (text: string) =>
waitFor(() =>
within(getElementByClassName('.rc-virtual-list')).getByText(text),
within(getElementByClassName('.ant-select-dropdown-list')).getByText(text),
);
const querySelectOption = (text: string) =>
waitFor(() =>
within(getElementByClassName('.rc-virtual-list')).queryByText(text),
within(getElementByClassName('.ant-select-dropdown-list')).queryByText(
text,
),
);
const findAllSelectOptions = () =>
@@ -644,7 +646,7 @@ test('does not add a new option if the option already exists', async () => {
await type(option);
await waitFor(() => {
const array = within(
getElementByClassName('.rc-virtual-list'),
getElementByClassName('.ant-select-dropdown-list'),
).getAllByText(option);
expect(array.length).toBe(1);
});
@@ -1398,7 +1400,7 @@ test('appends page>1 results during an active search and discards them when sear
// scrollTop via e.currentTarget in its onFallbackScroll handler, which
// then forwards to onPopupScroll (handlePagination here).
const holder = document.querySelector(
'.rc-virtual-list-holder',
'.ant-select-dropdown-list-holder',
) as HTMLElement | null;
if (!holder) throw new Error('virtual-list holder not rendered');
Object.defineProperty(holder, 'scrollHeight', {
@@ -93,12 +93,14 @@ const deselectAllButtonText = (length: number) =>
const findSelectOption = (text: string) =>
waitFor(() =>
within(getElementByClassName('.rc-virtual-list')).getByText(text),
within(getElementByClassName('.ant-select-dropdown-list')).getByText(text),
);
const querySelectOption = (text: string) =>
waitFor(() =>
within(getElementByClassName('.rc-virtual-list')).queryByText(text),
within(getElementByClassName('.ant-select-dropdown-list')).queryByText(
text,
),
);
const getAllSelectOptions = () =>
@@ -19,19 +19,71 @@
import { t } from '@apache-superset/core/translation';
import { sanitizeHtml } from './html';
export type TooltipTruncationMode = 'off' | 'end' | 'start' | 'middle';
export const TRUNCATION_MAX_CHARS = 40;
const TRUNCATION_STYLE = `
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
`;
const NOWRAP_STYLE = `
white-space: nowrap;
`;
/**
* Shortens plain text so a tooltip label stays readable, placing the ellipsis
* where the caller asked for it.
*
* Only 'start' and 'middle' slice. 'end' is handled by CSS in tooltipHtml, and
* 'off' means no truncation at all, so both return the input untouched.
*
* The input must be plain text. Callers are responsible for truncating before
* any markup (such as the ECharts series marker) is prepended, and before
* sanitization slicing a string that already contains markup would cut into
* a tag.
*/
export function truncateLabel(
text: string,
mode: TooltipTruncationMode = 'end',
): string {
if (
(mode !== 'start' && mode !== 'middle') ||
text.length <= TRUNCATION_MAX_CHARS
) {
return text;
}
const budget = TRUNCATION_MAX_CHARS - 1;
if (mode === 'start') {
return `${text.slice(-budget)}`;
}
const head = Math.ceil(budget / 2);
const tail = Math.floor(budget / 2);
return `${text.slice(0, head)}${text.slice(-tail)}`;
}
function getTruncationStyle(mode: TooltipTruncationMode): string {
if (mode === 'end') {
return TRUNCATION_STYLE;
}
if (mode === 'off') {
return '';
}
// 'start' and 'middle' are already sliced upstream; keep them on one line.
return NOWRAP_STYLE;
}
export function tooltipHtml(
data: string[][],
title?: string,
focusedRow?: number,
truncation: TooltipTruncationMode = 'end',
) {
const truncationStyle = getTruncationStyle(truncation);
const titleRow = title
? `<span style="font-weight: 700;${TRUNCATION_STYLE}">${title}</span>`
? `<span style="font-weight: 700;${truncationStyle}">${title}</span>`
: '';
return sanitizeHtml(`
<div>
@@ -46,7 +98,7 @@ export function tooltipHtml(
const cellStyle = `
text-align: ${j > 0 ? 'right' : 'left'};
padding-left: ${j === 0 ? 0 : 16}px;
${TRUNCATION_STYLE}
${truncationStyle}
`;
return `<td style="${cellStyle}">${cell}</td>`;
});
@@ -16,7 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { sanitizeHtml, tooltipHtml } from '@superset-ui/core';
import {
sanitizeHtml,
tooltipHtml,
truncateLabel,
TRUNCATION_MAX_CHARS,
} from '@superset-ui/core';
const TITLE_STYLE =
'style="font-weight: 700;max-width:300px;overflow:hidden;text-overflow:ellipsis;"';
@@ -182,3 +187,88 @@ test('should preserve table styling after sanitization (fixes ECharts tooltip fo
expect(html).toContain('padding-left:16px');
expect(html).toContain('max-width:300px');
});
describe('truncateLabel', () => {
const long = 'prod-us-east-1-service-checkout-latency-p99'; // 43 chars
test('returns text unchanged for off and end', () => {
expect(truncateLabel(long, 'off')).toBe(long);
expect(truncateLabel(long, 'end')).toBe(long);
});
test('defaults to end, which does not slice', () => {
expect(truncateLabel(long)).toBe(long);
});
test('truncates the start, keeping the distinguishing suffix', () => {
expect(truncateLabel(long, 'start')).toBe(
'…-us-east-1-service-checkout-latency-p99',
);
expect(truncateLabel(long, 'start')).toHaveLength(TRUNCATION_MAX_CHARS);
});
test('truncates the middle, keeping both ends', () => {
expect(truncateLabel(long, 'middle')).toBe(
'prod-us-east-1-servi…heckout-latency-p99',
);
expect(truncateLabel(long, 'middle')).toHaveLength(TRUNCATION_MAX_CHARS);
});
test('leaves text at or under the limit untouched', () => {
const atLimit = 'x'.repeat(TRUNCATION_MAX_CHARS);
expect(truncateLabel(atLimit, 'start')).toBe(atLimit);
expect(truncateLabel(atLimit, 'middle')).toBe(atLimit);
expect(truncateLabel('short', 'start')).toBe('short');
expect(truncateLabel('', 'middle')).toBe('');
});
test('truncates text one character over the limit', () => {
const overLimit = 'x'.repeat(TRUNCATION_MAX_CHARS + 1);
expect(truncateLabel(overLimit, 'start')).toBe(
`${'x'.repeat(TRUNCATION_MAX_CHARS - 1)}`,
);
});
});
describe('tooltipHtml truncation modes', () => {
const rows = [['label', 'value']];
// sanitizeHtml normalizes spacing inside style attributes, and it does so
// differently across versions, so compare with whitespace stripped.
const styles = (
title: string | undefined,
truncation?: 'off' | 'end' | 'start' | 'middle',
) => removeWhitespaces(tooltipHtml(rows, title, undefined, truncation));
test('emits the 300px cap for end and for the default', () => {
expect(styles('Title', 'end')).toContain('max-width:300px');
expect(tooltipHtml(rows, 'Title')).toBe(
tooltipHtml(rows, 'Title', undefined, 'end'),
);
});
test('emits no truncation style for off', () => {
const html = styles('Title', 'off');
expect(html).not.toContain('max-width');
expect(html).not.toContain('text-overflow');
expect(html).not.toContain('white-space');
});
test.each(['start', 'middle'] as const)(
'emits nowrap instead of a cap for %s',
mode => {
const html = styles('Title', mode);
expect(html).toContain('white-space:nowrap');
expect(html).not.toContain('max-width');
},
);
test('never slices cell text itself, whatever the mode', () => {
const longCell = 'y'.repeat(TRUNCATION_MAX_CHARS + 20);
(['off', 'end', 'start', 'middle'] as const).forEach(mode => {
expect(tooltipHtml([[longCell]], undefined, undefined, mode)).toContain(
longCell,
);
});
});
});
+4
View File
@@ -47,6 +47,10 @@ export default defineConfig({
// Retry logic - 2 retries in CI, 0 locally
retries: process.env.CI ? 2 : 0,
// Disable capturing Git commit info as the project's history is increasingly dense
// and breach Playwright's default 3-seconds `git` command timeout limit
captureGitInfo: { commit: false, diff: false },
// Reporter configuration - multiple reporters for better visibility
reporter: process.env.CI
? [
@@ -22,7 +22,7 @@ import { Modal } from '../core/Modal';
/**
* Confirm Dialog component for Ant Design Modal.confirm dialogs.
* These are the "OK" / "Cancel" confirmation dialogs used throughout Superset.
* These are the "Confirm" / "Cancel" confirmation dialogs used throughout Superset.
* Uses getByRole with name to target specific confirm dialogs when multiple are open.
*/
export class ConfirmDialog extends Modal {
@@ -43,7 +43,7 @@ export class ConfirmDialog extends Modal {
}
/**
* Clicks the OK button to confirm.
* Clicks the Confirm button to confirm.
* @param options.timeout - If provided, silently returns if dialog doesn't appear
* within timeout. If not provided, waits indefinitely (strict mode).
*/
@@ -53,7 +53,7 @@ export class ConfirmDialog extends Modal {
state: 'visible',
timeout: options?.timeout,
});
await this.clickFooterButton('OK');
await this.clickFooterButton('Confirm');
await this.waitForHidden();
} catch (error) {
// Only swallow TimeoutError when timeout was explicitly provided
@@ -1,58 +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.
*/
/**
* With SOFT_DELETE enabled the delete-confirmation modal becomes recoverable:
* it explains the object is moved to the archive (and for how long), and drops
* the "type DELETE to confirm" friction. Non-destructive the modal is opened
* and dismissed without deleting anything.
*/
import { test, expect } from '@playwright/test';
import { skipUnlessFeatureEnabled } from '../../helpers/featureFlags';
test.beforeEach(async ({ page }) => {
await skipUnlessFeatureEnabled(page, 'SOFT_DELETE');
});
test('chart delete confirmation reflects soft-delete (archive) semantics', async ({
page,
}) => {
await page.goto('chart/list/');
await page.locator('[data-test="chart-row-delete"]').first().waitFor();
await page.locator('[data-test="chart-row-delete"]').first().click();
// The action reads as "Archive", not "Delete". Scope to the dialog: with
// the flag on, every list row's delete action is also named "Archive", so
// an unscoped button query is a strict-mode violation (25 rows + modal).
const dialog = page.getByRole('dialog');
await expect(dialog.getByText(/^Archive .+\?$/)).toBeVisible();
await expect(dialog.getByRole('button', { name: 'Archive' })).toBeVisible();
// Recoverable copy instead of "Are you sure … permanently".
await expect(page.getByText(/moved to Recently Archived/i)).toBeVisible();
await expect(
page.getByText(/recover it there within \d+ days/i),
).toBeVisible();
// No "type DELETE to confirm" input in recoverable mode.
await expect(page.getByTestId('delete-modal-input')).toHaveCount(0);
// Dismiss without deleting.
await page.getByTestId('close-modal-btn').click();
});
@@ -29,7 +29,7 @@
* restore it and asserts via the API that it is live again.
*/
import { test, expect, Page } from '@playwright/test';
import { apiGet, apiPost } from '../../helpers/api/requests';
import { apiGet } from '../../helpers/api/requests';
import { extractIdFromResponse } from '../../helpers/api/assertions';
import {
apiPostChart,
@@ -188,58 +188,3 @@ test('permanently deletes an archived item from the view', async ({ page }) => {
await TYPES[0].softDelete(page, id).catch(() => {});
}
});
test('shows an empty message and no rows when the search matches nothing', async ({
page,
}) => {
await page.goto('archived/');
await expect(page.getByTestId('archived-list-view')).toBeVisible();
const search = page.getByPlaceholder(/type a value/i);
await search.click();
await search.fill(`e2e_nonexistent_${Date.now()}`);
await search.press('Enter');
await expect(
page.getByText('No results match your filter criteria'),
).toBeVisible();
await expect(page.getByTestId('archived-row-restore')).toHaveCount(0);
});
test('restoring an already-restored row surfaces an error without crashing', async ({
page,
}) => {
const name = `e2e_stale_${Date.now()}`;
const id = await TYPES[0].create(page, name);
// Capture the uuid before soft-delete (a soft-deleted GET returns 404).
const { uuid } = (await (await apiGetDashboard(page, id)).json()).result;
try {
expect((await apiDeleteDashboard(page, id)).ok()).toBeTruthy();
await openArchive(page, 'Dashboard', name);
await expect(page.getByText(name, { exact: false })).toBeVisible();
// Simulate another actor restoring the object out from under this view.
const restored = await apiPost(
page,
`api/v1/dashboard/${uuid}/restore`,
{},
);
expect(restored.ok()).toBeTruthy();
// Clicking the now-stale row's Restore yields a 404 → danger toast, no crash.
await page
.getByRole('row')
.filter({ hasText: name })
.getByTestId('archived-row-restore')
.click();
await expect(
page.getByText(`Failed to restore ${name}`, { exact: false }),
).toBeVisible({ timeout: 15000 });
// The page is still functional (the list view did not crash).
await expect(page.getByTestId('archived-list-view')).toBeVisible();
} finally {
// Re-archive the (possibly) restored dashboard, whatever happened above.
await apiDeleteDashboard(page, id).catch(() => {});
}
});
@@ -22,7 +22,7 @@ import { getSequentialSchemeRegistry } from '@superset-ui/core';
import { SupersetTheme } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import CalHeatMapImport from './vendor/cal-heatmap';
import { convertUTCTimestampToLocal } from './utils';
import { convertUTCTimestampToLocal, getFormattedUTCTime } from './utils';
// The vendor file is @ts-nocheck, so its export lacks type info.
// Define a minimal constructor interface for use in this file.
@@ -103,6 +103,8 @@ function Calendar(element: HTMLElement, props: CalendarProps) {
const subDomainTextFormat = showValues
? (_date: Date, value: number) => valueFormatter(value)
: null;
const dateFormatter = (date: Date, format: string) =>
getFormattedUTCTime(date.getTime(), format);
const metricsData = data.data;
@@ -166,6 +168,7 @@ function Calendar(element: HTMLElement, props: CalendarProps) {
itemName: '',
valueFormatter,
timeFormatter,
dateFormatter,
subDomainTextFormat,
});
});
@@ -76,6 +76,8 @@ var CalHeatMap = function () {
timeFormatter: d => d,
dateFormatter: null,
domain: 'hour',
subDomain: 'min',
@@ -1990,10 +1992,14 @@ CalHeatMap.prototype = {
if (typeof format === 'function') {
return format(d);
} else {
var f = d3.time.format(format);
return f(d);
}
if (typeof this.options.dateFormatter === 'function') {
return this.options.dateFormatter(d, format);
}
var f = d3.time.format(format);
return f(d);
},
getSubDomainTitle: function (d) {
@@ -25,9 +25,11 @@ import {
waitFor,
} from 'spec/helpers/testing-library';
import { CALENDAR_TOOLTIP_CLASS } from '../src/tooltip';
import { convertUTCTimestampToLocal } from '../src/utils';
interface MockCalHeatMapConfig {
itemSelector: Element;
dateFormatter?: (date: Date, format: string) => string;
}
type MetricNameInput = string | string[];
@@ -38,6 +40,7 @@ let mockInitCallCount = 0;
let mockThrowOnInitCall: number | null = null;
let mockDestroyCallCount = 0;
let mockDestroyedInstanceIds: string[] = [];
let mockDateFormatter: MockCalHeatMapConfig['dateFormatter'];
const mockTheme = {
colorBgElevated: '#ffffff',
@@ -56,6 +59,7 @@ jest.mock('../src/vendor/cal-heatmap', () => ({
} = require('../src/tooltip');
mockInitCallCount += 1;
mockDateFormatter = config.dateFormatter;
if (mockThrowOnInitCall === mockInitCallCount) {
throw new Error('Mock CalHeatMap init failure');
}
@@ -284,9 +288,28 @@ afterEach(() => {
mockThrowOnInitCall = null;
mockDestroyCallCount = 0;
mockDestroyedInstanceIds = [];
mockDateFormatter = undefined;
document.body.innerHTML = '';
});
test('Calendar provides a timezone-safe date formatter to CalHeatMap', () => {
const calendarOwner = document.createElement('div');
document.body.appendChild(calendarOwner);
Calendar(calendarOwner, {
...createCalendarProps('localized-metric'),
theme: mockTheme,
});
if (!mockDateFormatter) {
throw new Error('Expected Calendar to configure a date formatter');
}
const localDate = new Date(convertUTCTimestampToLocal(Date.UTC(2024, 0, 1)));
expect(mockDateFormatter(localDate, '%Y-%m-%d')).toBe('2024-01-01');
});
test('rerender and unmount clean up only the affected calendar tooltips', () => {
jest.useFakeTimers();
@@ -0,0 +1,61 @@
/**
* 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 CalHeatMapImport from '../src/vendor/cal-heatmap';
type DateFormatter = (date: Date, format: string) => string;
type FunctionalDateFormat = (date: Date) => string;
interface CalHeatMapInstance {
options: {
dateFormatter: DateFormatter | null;
};
formatDate(date: Date, format: string | FunctionalDateFormat): string;
}
const CalHeatMap = CalHeatMapImport as unknown as new () => CalHeatMapInstance;
test('CalHeatMap delegates string date formats to the configured formatter', () => {
const calendar = new CalHeatMap();
const date = new Date(2024, 0, 1);
const dateFormatter = jest.fn<string, [Date, string]>(() => 'Январь');
calendar.options.dateFormatter = dateFormatter;
expect(calendar.formatDate(date, '%B')).toBe('Январь');
expect(dateFormatter).toHaveBeenCalledWith(date, '%B');
});
test('CalHeatMap preserves functional formatters over the configured formatter', () => {
const calendar = new CalHeatMap();
const date = new Date(2024, 0, 1);
const dateFormatter = jest.fn<string, [Date, string]>(() => 'localized');
const functionalFormat = jest.fn<string, [Date]>(() => 'custom');
calendar.options.dateFormatter = dateFormatter;
expect(calendar.formatDate(date, functionalFormat)).toBe('custom');
expect(functionalFormat).toHaveBeenCalledWith(date);
expect(dateFormatter).not.toHaveBeenCalled();
});
test('CalHeatMap keeps the D3 formatter fallback', () => {
const calendar = new CalHeatMap();
const date = new Date(2024, 0, 1);
expect(calendar.formatDate(date, '%B')).toBe('January');
});
@@ -30,12 +30,12 @@
},
"dependencies": {
"d3": "^3.5.17",
"prop-types": "^15.8.1",
"react": "^19.2.7"
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*"
"@superset-ui/core": "*",
"react": "^18.3.0"
}
}
@@ -17,6 +17,10 @@
* under the License.
*/
import { getNumberFormatter } from '@superset-ui/core';
import { render, fireEvent } from '../../../../spec/helpers/testing-library';
import BigNumberVis from './BigNumberViz';
/**
* Tests for the color threshold formatter logic in BigNumberViz.
*
@@ -83,3 +87,33 @@ describe('BigNumberViz color formatters', () => {
expect(getColorFromValue).not.toHaveBeenCalled();
});
});
describe('BigNumberViz context menu', () => {
test('invokes onContextMenu and stops the event bubbling to ancestor handlers', () => {
const onContextMenu = jest.fn();
const ancestorHandler = jest.fn();
const { container } = render(
<div onContextMenu={ancestorHandler}>
<BigNumberVis
width={200}
height={100}
bigNumber={42}
headerFormatter={getNumberFormatter()}
headerFontSize={0.3}
subheaderFontSize={0.125}
subtitleFontSize={0.125}
subtitle=""
refs={{}}
onContextMenu={onContextMenu}
/>
</div>,
);
const headerLine = container.querySelector('.header-line');
fireEvent.contextMenu(headerLine!, { clientX: 10, clientY: 20 });
expect(onContextMenu).toHaveBeenCalledWith(10, 20);
expect(ancestorHandler).not.toHaveBeenCalled();
});
});
@@ -224,6 +224,7 @@ function BigNumberVis({
const handleContextMenu = (e: MouseEvent<HTMLDivElement>) => {
if (onContextMenu) {
e.preventDefault();
e.stopPropagation();
onContextMenu(e.nativeEvent.clientX, e.nativeEvent.clientY);
}
};
@@ -0,0 +1,70 @@
/**
* 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 { SqlaFormData } from '@superset-ui/core';
import transformProps from './transformProps';
import { EchartsBubbleChartProps } from './types';
const baseFormData: SqlaFormData = {
datasource: '1__table',
viz_type: 'bubble_v2',
entity: 'customer_name',
x: 'price',
y: 'sales',
size: 'count',
};
const baseChartProps = {
width: 400,
height: 400,
hooks: {},
queriesData: [
{
data: [
{ customer_name: 'A', price: 10, sales: 100, count: 5 },
{ customer_name: 'B', price: 20, sales: 200, count: 8 },
],
},
],
theme: { colorText: '#000' },
};
test('nests xAxisLabelInterval under axisLabel rather than the axis itself', () => {
// Regression test: xAxis.interval forces echarts' IntervalScale into a
// fixed-tick-spacing mode that expects a number and crashes on the
// 'auto'/'0' strings this control actually produces (observed as an
// uncaught assertion deep in echarts' axis "nice" tick calculation,
// reproducing on every dashboard bubble chart). The interval belongs on
// axisLabel, where it only controls how many labels are skipped.
const { echartOptions } = transformProps({
...baseChartProps,
formData: baseFormData,
} as unknown as EchartsBubbleChartProps);
expect((echartOptions.xAxis as any).interval).toBeUndefined();
expect((echartOptions.xAxis as any).axisLabel.interval).toBe('auto');
});
test('honors an explicit xAxisLabelInterval override', () => {
const { echartOptions } = transformProps({
...baseChartProps,
formData: { ...baseFormData, xAxisLabelInterval: '0' },
} as unknown as EchartsBubbleChartProps);
expect((echartOptions.xAxis as any).axisLabel.interval).toBe('0');
});
@@ -212,13 +212,16 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
const echartOptions: EChartsCoreOption = {
series,
xAxis: {
axisLabel: { formatter: xAxisFormatter, rotate: xAxisLabelRotation },
axisLabel: {
formatter: xAxisFormatter,
rotate: xAxisLabelRotation,
interval: xAxisLabelInterval,
},
splitLine: {
lineStyle: {
type: 'dashed',
},
},
interval: xAxisLabelInterval,
scale: true,
name: bubbleXAxisTitle,
nameLocation: 'middle',
@@ -390,6 +390,7 @@ export default function transformProps(chartProps: EchartsGanttChartProps) {
[GenericDataType.String]: undefined,
[GenericDataType.Temporal]: tooltipTimeFormatter,
[GenericDataType.Boolean]: undefined,
[GenericDataType.MultiValue]: undefined,
};
const echartOptions: EChartsCoreOption = {
@@ -40,6 +40,7 @@ import {
TimeseriesChartDataResponseResult,
TimeseriesDataRecord,
tooltipHtml,
truncateLabel,
ValueFormatter,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
@@ -79,6 +80,7 @@ import {
getAnnotationData,
} from '../utils/annotation';
import {
collapseForecastKeys,
extractForecastSeriesContext,
extractForecastValuesFromTooltipParams,
formatForecastTooltipSeries,
@@ -207,6 +209,7 @@ export default function transformProps(
zoomable,
richTooltip,
tooltipSortByMetric,
tooltipTruncation,
xAxisBounds,
xAxisLabelRotation,
xAxisLabelInterval,
@@ -859,12 +862,14 @@ export default function transformProps(
: params.value[0];
const forecastValue: any[] = richTooltip ? params : [params];
const sortedKeys = extractTooltipKeys(
forecastValue,
// horizontal mode is not supported in mixed series chart
1,
richTooltip,
tooltipSortByMetric,
const sortedKeys = collapseForecastKeys(
extractTooltipKeys(
forecastValue,
// horizontal mode is not supported in mixed series chart
1,
richTooltip,
tooltipSortByMetric,
),
);
const rows: string[][] = [];
@@ -907,13 +912,19 @@ export default function transformProps(
formatter: primarySeries.has(key)
? tooltipFormatter
: tooltipFormatterSecondary,
truncation: tooltipTruncation,
});
rows.push(row);
if (key === focusedSeries) {
focusedRow = rows.length - 1;
}
});
return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow);
return tooltipHtml(
rows,
truncateLabel(tooltipFormatter(xValue), tooltipTruncation),
focusedRow,
tooltipTruncation,
);
},
},
legend: {
@@ -24,6 +24,7 @@ import {
ContributionType,
TimeFormatter,
AxisType,
TooltipTruncationMode,
} from '@superset-ui/core';
import {
BaseChartProps,
@@ -59,6 +60,7 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & {
timeGrainSqla?: TimeGranularity;
forceMaxInterval?: boolean;
tooltipTimeFormat?: string;
tooltipTruncation?: TooltipTruncationMode;
zoomable: boolean;
richTooltip: boolean;
showQueryIdentifiers?: boolean;
@@ -108,6 +110,7 @@ export const DEFAULT_FORM_DATA: EchartsMixedTimeseriesFormData = {
yAxisFormatSecondary: TIMESERIES_DEFAULTS.yAxisFormat,
yAxisTitleSecondary: DEFAULT_TITLE_FORM_DATA.yAxisTitle,
tooltipTimeFormat: TIMESERIES_DEFAULTS.tooltipTimeFormat,
tooltipTruncation: TIMESERIES_DEFAULTS.tooltipTruncation,
xAxisBounds: TIMESERIES_DEFAULTS.xAxisBounds,
xAxisForceCategorical: TIMESERIES_DEFAULTS.xAxisForceCategorical,
xAxisTimeFormat: TIMESERIES_DEFAULTS.xAxisTimeFormat,
@@ -73,6 +73,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
seriesType: EchartsTimeseriesSeriesType.Line,
stack: false,
tooltipTimeFormat: 'smart_date',
tooltipTruncation: 'end',
xAxisTimeFormat: 'smart_date',
xAxisNumberFormat: 'SMART_NUMBER',
truncateXAxis: true,
@@ -30,6 +30,7 @@ import {
DTTM_ALIAS,
ensureIsArray,
tooltipHtml,
truncateLabel,
getCustomFormatter,
getMetricLabel,
getNumberFormatter,
@@ -94,6 +95,7 @@ import {
getAnnotationData,
} from '../utils/annotation';
import {
collapseForecastKeys,
extractForecastSeriesContext,
extractForecastSeriesContexts,
extractForecastValuesFromTooltipParams,
@@ -303,6 +305,7 @@ export default function transformProps(
tooltipSortByMetric,
showTooltipTotal,
showTooltipPercentage,
tooltipTruncation,
truncateXAxis,
truncateYAxis,
xAxis: xAxisOrig,
@@ -1390,11 +1393,13 @@ export default function transformProps(
const forecastValue: CallbackDataParams[] = richTooltip
? params
: [params];
const sortedKeys = extractTooltipKeys(
forecastValue,
yIndex,
richTooltip,
tooltipSortByMetric,
const sortedKeys = collapseForecastKeys(
extractTooltipKeys(
forecastValue,
yIndex,
richTooltip,
tooltipSortByMetric,
),
);
const filteredForecastValue = forecastValue.filter(
(item: CallbackDataParams) =>
@@ -1449,6 +1454,7 @@ export default function transformProps(
seriesName: key,
formatter,
marker,
truncation: tooltipTruncation,
});
const annotationRow = annotationLayers.some(
@@ -1482,7 +1488,12 @@ export default function transformProps(
}
rows.push(totalRow);
}
return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow);
return tooltipHtml(
rows,
truncateLabel(tooltipFormatter(xValue), tooltipTruncation),
focusedRow,
tooltipTruncation,
);
},
},
legend: {
@@ -467,6 +467,14 @@ export function transformSeries(
return formatter(numericValue);
}
if (!onlyTotal) {
// A stacked segment with no height begins and ends at the same
// coordinate as the top of the segment beneath it, so its label is
// drawn over that segment's label. Zero and null have no height, so
// they carry no label. The rich tooltip omits zero observations from
// a stacked series for the same reason.
if (stack && !numericValue) {
return '';
}
if (
numericValue >=
(thresholdValues[dataIndex] || Number.MIN_SAFE_INTEGER)
@@ -25,6 +25,7 @@ import {
QueryFormMetric,
TimeFormatter,
TimeGranularity,
TooltipTruncationMode,
} from '@superset-ui/core';
import {
BaseChartProps,
@@ -82,6 +83,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
tooltipTimeFormat?: string;
showTooltipTotal?: boolean;
showTooltipPercentage?: boolean;
tooltipTruncation?: TooltipTruncationMode;
truncateXAxis: boolean;
truncateYAxis: boolean;
yAxisFormat?: string;
@@ -16,9 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, waitFor } from '../../../../spec/helpers/testing-library';
import type { EChartsCoreOption } from 'echarts/core';
import Echart, { isReportScreenshotMode } from './Echart';
import { render, waitFor } from '../../../../spec/helpers/testing-library';
import Echart, {
ECHARTS_HOST_CLASS,
ECHARTS_RENDER_FINISHED_CLASS,
isReportScreenshotMode,
} from './Echart';
import type { EchartsProps } from '../types';
type Handler = (params: unknown) => void;
@@ -272,3 +276,31 @@ test('keeps animation enabled when not in report screenshot mode', async () => {
const lastOptions = mockChart.setOption.mock.calls.at(-1)?.[0];
expect(lastOptions.animation).not.toBe(false);
});
test('tags the ECharts canvas host with the readiness-gate class', async () => {
const { container } = render(renderEchart(), {
initialState,
useRedux: true,
});
await waitFor(() => expect(mockChart.setOption).toHaveBeenCalled());
expect(container.querySelector(`.${ECHARTS_HOST_CLASS}`)).not.toBeNull();
});
test('marks the host painted only on the ECharts `finished` event', async () => {
const { container } = render(renderEchart(), {
initialState,
useRedux: true,
});
await waitFor(() => expect(mockChart.setOption).toHaveBeenCalled());
const host = container.querySelector(`.${ECHARTS_HOST_CLASS}`) as HTMLElement;
expect(host).not.toBeNull();
// `setOption` ran during mount, which clears the marker; `finished` has not
// fired yet, so the host must NOT be flagged as painted.
expect(host).not.toHaveClass(ECHARTS_RENDER_FINISHED_CLASS);
// Simulate ECharts completing its draw -> the host is flagged painted.
trigger('finished');
expect(host).toHaveClass(ECHARTS_RENDER_FINISHED_CLASS);
});
@@ -138,6 +138,15 @@ export function isReportScreenshotMode(): boolean {
}
}
// Report-screenshot readiness contract (see superset/utils/screenshot_utils.py).
// `echarts-host` marks the canvas host element; `echarts-render-finished` is
// toggled OFF before each setOption and ON in the ECharts `finished` event --
// the only signal that the canvas is fully painted (chartStatus/onRenderSuccess
// both fire pre-paint). The readiness gate treats a host that lacks
// `echarts-render-finished` as not-yet-painted so it never captures a blank chart.
export const ECHARTS_HOST_CLASS = 'echarts-host';
export const ECHARTS_RENDER_FINISHED_CLASS = 'echarts-render-finished';
function Echart(
{
width,
@@ -201,6 +210,11 @@ function Echart(
width,
height,
});
// Paint marker for the report-screenshot readiness gate. `finished`
// is the only event that guarantees the canvas is fully drawn.
chartRef.current.on('finished', () => {
divRef.current?.classList.add(ECHARTS_RENDER_FINISHED_CLASS);
});
}
// did mount
handleSizeChange({ width, height });
@@ -321,6 +335,9 @@ function Echart(
}
)?.dataZoom
: undefined;
// Clear the paint marker before (re)drawing; the `finished` handler
// re-adds it once the new frame is fully rendered.
divRef.current?.classList.remove(ECHARTS_RENDER_FINISHED_CLASS);
chartRef.current?.setOption(themedEchartOptions, {
notMerge,
replaceMerge: notMerge ? undefined : ['series'],
@@ -412,7 +429,14 @@ function Echart(
handleSizeChange({ width, height });
}, [width, height, handleSizeChange]);
return <Styles ref={divRef} height={height} width={width} />;
return (
<Styles
ref={divRef}
className={ECHARTS_HOST_CLASS}
height={height}
width={width}
/>
);
}
export default forwardRef(Echart);
@@ -315,6 +315,27 @@ const tooltipPercentageControl: ControlSetItem = {
},
};
const tooltipTruncationControl: ControlSetItem = {
name: 'tooltipTruncation',
config: {
type: 'SelectControl',
freeForm: false,
label: t('Truncate labels'),
renderTrigger: true,
default: 'end',
clearable: false,
choices: [
['off', t('Off')],
['end', t('End')],
['start', t('Start')],
['middle', t('Middle')],
],
description: t(
'Where to place the ellipsis when a tooltip label is too long. Choose Off to always show the full label, or Start when labels share a common prefix.',
),
},
};
export const richTooltipSection: ControlSetRow[] = [
[<ControlSubSectionHeader>{t('Tooltip')}</ControlSubSectionHeader>],
[richTooltipControl],
@@ -322,6 +343,7 @@ export const richTooltipSection: ControlSetRow[] = [
[tooltipPercentageControl],
[tooltipSortByMetricControl],
[tooltipTimeFormatControl],
[tooltipTruncationControl],
];
const sortSeriesType: ControlSetItem = {
@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { DataRecord, DTTM_ALIAS, ValueFormatter } from '@superset-ui/core';
import {
DataRecord,
DTTM_ALIAS,
truncateLabel,
TooltipTruncationMode,
ValueFormatter,
} from '@superset-ui/core';
import type { OptionName, SeriesOption } from 'echarts/types/src/util/types';
import type { TooltipMarker } from 'echarts/types/src/util/format';
import {
@@ -54,6 +60,21 @@ export const extractForecastSeriesContexts = (
{} as { [key: string]: ForecastSeriesEnum[] },
);
/**
* Collapses raw ECharts series ids onto the names used to key tooltip rows.
*
* Tooltip values are grouped by forecast-stripped name, so any ordering derived
* from the raw series ids has to be expressed in the same terms before it can be
* matched against them. This matters beyond real Prophet output: a metric simply
* labelled `ci__yhat_lower` collapses to `ci` exactly like a forecast bound
* does, and a chart whose every series carries such a suffix has no id that
* survives the comparison untouched.
*/
export const collapseForecastKeys = (seriesIds: string[]): string[] =>
Array.from(
new Set(seriesIds.map(id => extractForecastSeriesContext(id).name)),
);
export const extractForecastValuesFromTooltipParams = (
params: any[],
isHorizontal = false,
@@ -91,12 +112,16 @@ export const formatForecastTooltipSeries = ({
forecastUpper,
marker,
formatter,
truncation = 'end',
}: ForecastValue & {
seriesName: string;
marker: TooltipMarker;
formatter: ValueFormatter;
truncation?: TooltipTruncationMode;
}): string[] => {
const name = `${marker}${sanitizeHtml(seriesName)}`;
// Truncate before sanitizing and before the marker is prepended: slicing a
// string that already contains markup would cut into the marker's tag.
const name = `${marker}${sanitizeHtml(truncateLabel(seriesName, truncation))}`;
let value = typeof observation === 'number' ? formatter(observation) : '';
// Use finite-number checks rather than truthiness so that legitimate
// zero values (e.g. a forecast that crosses zero, or a confidence bound of
@@ -27,6 +27,7 @@ import {
VizType,
ChartDataResponseResult,
TimeGranularity,
TooltipTruncationMode,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import {
@@ -1295,3 +1296,59 @@ test('y-axis title position: non-Left sets nameLocation to end', () => {
expect(yAxis[1].nameGap).toEqual(30);
expect(yAxis[1].nameLocation).toEqual('end');
});
describe('EchartsMixedTimeseries tooltip truncation', () => {
const longSeriesName = 'prod-us-east-1-service-checkout-latency-p99';
const marker = '<span style="background-color:#1f77b4;"></span>';
const buildTooltip = (tooltipTruncation?: TooltipTruncationMode) => {
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: queriesData,
formData: {
...formData,
...(tooltipTruncation ? { tooltipTruncation } : {}),
},
queriesData,
});
const { echartOptions } = transformProps(chartProps);
const { formatter } = echartOptions.tooltip as {
formatter: (params: unknown) => string;
};
// richTooltip is false in this fixture, so the trigger is 'item' and the
// formatter receives a single param object rather than an array.
return formatter({
seriesId: longSeriesName,
seriesName: longSeriesName,
value: [599616000000, 1],
marker,
});
};
test('keeps full text with the CSS cap by default', () => {
const html = buildTooltip();
expect(html.replace(/\s/g, '')).toContain('max-width:300px');
expect(html).toContain(longSeriesName);
});
test('removes the cap and keeps full text when off', () => {
const html = buildTooltip('off');
expect(html).not.toContain('max-width');
expect(html).toContain(longSeriesName);
});
test('drops the shared prefix when truncating from the start', () => {
const html = buildTooltip('start');
expect(html).not.toContain('prod-us-east');
expect(html).toContain('latency-p99');
expect(html).toContain('background-color:#1f77b4');
});
test('keeps both ends when truncating the middle', () => {
const html = buildTooltip('middle');
expect(html).toContain('prod-us-east-1-servi…heckout-latency-p99');
expect(html).not.toContain(longSeriesName);
});
});
@@ -32,6 +32,7 @@ import {
TimeseriesAnnotationLayer,
ChartDataResponseResult,
TimeGranularity,
TooltipTruncationMode,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import { supersetTheme } from '@apache-superset/core/theme';
@@ -2437,3 +2438,155 @@ test('honors the snake_case flag the compare-chart migration stores in params',
[BASE_TIMESTAMP + 300000000, 2],
]);
});
describe('EchartsTimeseries tooltip truncation', () => {
const longSeriesName = 'prod-us-east-1-service-checkout-latency-p99';
const marker = '<span style="background-color:#1f77b4;"></span>';
const buildTooltip = (
tooltipTruncation?: TooltipTruncationMode,
xValue: string | number = 599616000000,
) => {
const chartProps = new ChartProps({
formData: {
colorScheme: 'bnbColors',
datasource: '3__table',
granularity_sqla: 'ds',
metric: 'sum__num',
groupby: ['foo'],
viz_type: 'my_viz',
...(tooltipTruncation ? { tooltipTruncation } : {}),
} as SqlaFormData,
width: 800,
height: 600,
queriesData: [
{
data: [
{ [longSeriesName]: 1, __timestamp: 599616000000 },
{ [longSeriesName]: 3, __timestamp: 599916000000 },
],
},
],
theme: supersetTheme,
});
const { echartOptions } = transformProps(
chartProps as EchartsTimeseriesChartProps,
);
const { formatter } = echartOptions.tooltip as {
formatter: (params: unknown) => string;
};
return formatter([
{
seriesId: longSeriesName,
seriesName: longSeriesName,
value: [xValue, 1],
marker,
},
]);
};
test('applies the CSS cap and keeps full text by default', () => {
const html = buildTooltip();
expect(html).toContain(longSeriesName);
// sanitizeHtml normalizes spacing inside style attributes, so compare with
// whitespace stripped rather than hard-coding one version's formatting.
expect(html.replace(/\s/g, '')).toContain('max-width:300px');
});
test('removes the cap and keeps full text when off', () => {
const html = buildTooltip('off');
expect(html).not.toContain('max-width');
expect(html).toContain(longSeriesName);
});
test('drops the shared prefix when truncating from the start', () => {
const html = buildTooltip('start');
expect(html).not.toContain('prod-us-east');
expect(html).toContain('latency-p99');
expect(html.replace(/\s/g, '')).toContain('white-space:nowrap');
});
test('keeps both ends when truncating the middle', () => {
const html = buildTooltip('middle');
expect(html).toContain('prod-us-east-1-servi…heckout-latency-p99');
expect(html).not.toContain(longSeriesName);
});
test('preserves the echarts marker in every mode', () => {
(['off', 'end', 'start', 'middle'] as const).forEach(mode => {
expect(buildTooltip(mode)).toContain('background-color:#1f77b4');
});
});
test('truncates a long non-temporal x-axis title', () => {
const longCategory = 'prod-us-east-1-service-checkout-cohort-2026';
const html = buildTooltip('start', longCategory);
expect(html).not.toContain(longCategory);
expect(html).toContain('cohort-2026');
});
test('leaves a long title alone in the default mode', () => {
const longCategory = 'prod-us-east-1-service-checkout-cohort-2026';
expect(buildTooltip(undefined, longCategory)).toContain(longCategory);
});
});
describe('tooltip for metrics whose labels end in forecast suffixes', () => {
const marker = '<span style="background-color:#1f77b4;"></span>';
const seriesIds = ['ci__yhat', 'ci__yhat_lower', 'ci__yhat_upper'];
const values = [1.5, 0.5, 2.0];
// Metrics can be labelled `ci__yhat*` with no forecast enabled and no plain
// observation series. Every series then collapses onto the same
// forecast-stripped tooltip key, so no raw series id matches itself.
const buildTooltip = (tooltipSortByMetric = false) => {
const chartProps = createTestChartProps({
formData: {
x_axis: 'dt',
metrics: seriesIds,
groupby: [],
richTooltip: true,
tooltipSortByMetric,
} as Partial<EchartsTimeseriesFormData>,
queriesData: [
createTestQueryData([
{
dt: 599616000000,
ci__yhat: 1.5,
ci__yhat_lower: 0.5,
ci__yhat_upper: 2.5,
},
]),
],
});
const tooltipFormatter = (transformProps(chartProps).echartOptions as any)
.tooltip.formatter;
return tooltipFormatter(
seriesIds.map((id, i) => ({
seriesId: id,
seriesName: id,
value: [599616000000, values[i]],
data: [599616000000, values[i]],
marker,
})),
);
};
test('renders the collapsed series rather than falling back to "No data"', () => {
const html = buildTooltip();
expect(html).not.toContain('No data');
expect(html).toContain('>ci<');
expect(html).toContain('ŷ = 1.5 (0.5, 2.5)');
});
test('renders a single row rather than one per forecast suffix', () => {
const html = buildTooltip();
expect(html.match(/<tr/g)).toHaveLength(1);
expect(html).toContain('>ci<');
});
test('still renders the row when the tooltip is sorted by metric', () => {
const html = buildTooltip(true);
expect(html).not.toContain('No data');
expect(html).toContain('>ci<');
});
});
@@ -20,13 +20,14 @@ import {
CategoricalColorScale,
ChartProps,
TimeGranularity,
getNumberFormatter,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import { supersetTheme } from '@apache-superset/core/theme';
import type { SeriesOption } from 'echarts';
import type { ScatterSeriesOption } from 'echarts/charts';
import { EchartsTimeseriesSeriesType } from '../../src';
import { TIMESERIES_CONSTANTS } from '../../src/constants';
import { StackControlsValue, TIMESERIES_CONSTANTS } from '../../src/constants';
import {
LegendOrientation,
EchartsTimeseriesChartProps,
@@ -566,3 +567,70 @@ test('getPadding should handle Left position with zero margin correctly', () =>
getChartPaddingSpy.mockRestore();
}
});
/**
* #42702: a stacked segment with no height starts and ends at the same
* coordinate as the top of the segment beneath it, so a value label on it is
* drawn over that segment's label. `percentage_threshold` does not filter these
* out: it defaults to 0, and `thresholdValues[dataIndex] || MIN_SAFE_INTEGER`
* turns a 0 threshold into "no filtering", which is intentional.
*/
const stackedLabel = (
numericValue: number | null,
opts: Record<string, unknown> = {},
) => {
const series = transformSeries(
{ id: 'B', name: 'B', data: [[1, numericValue]] } as SeriesOption,
mockColorScale,
'B',
{
seriesType: EchartsTimeseriesSeriesType.Bar,
stack: StackControlsValue.Stack,
showValue: true,
onlyTotal: false,
formatter: getNumberFormatter(),
thresholdValues: [0],
...opts,
},
) as SeriesOption & {
label: { formatter: (params: unknown) => string };
};
return series.label.formatter({
value: [1, numericValue],
dataIndex: 0,
seriesIndex: 1,
seriesName: 'B',
});
};
test('stacked value labels are omitted for a zero-height segment', () => {
expect(stackedLabel(0)).toBe('');
expect(stackedLabel(null)).toBe('');
});
test('stacked value labels are kept for segments that have height', () => {
expect(stackedLabel(32)).toBe('32');
expect(stackedLabel(-5)).toBe('-5');
});
test('a zero value keeps its label when the series is not stacked', () => {
// Without a stack the label sits on the bar itself, so there is nothing for
// it to collide with.
expect(stackedLabel(0, { stack: undefined })).toBe('0');
});
test('percentage_threshold still filters values below the threshold', () => {
// 10% of a 100 total. The zero-height guard must not swallow this rule.
expect(stackedLabel(5, { thresholdValues: [10] })).toBe('');
expect(stackedLabel(50, { thresholdValues: [10] })).toBe('50');
});
test('only-total labels are unaffected by the zero-height guard', () => {
expect(
stackedLabel(0, {
onlyTotal: true,
showValueIndexes: [1],
totalStackedValues: [32],
}),
).toBe('32');
});
@@ -16,9 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import { getNumberFormatter, NumberFormats } from '@superset-ui/core';
import {
getNumberFormatter,
NumberFormats,
TRUNCATION_MAX_CHARS,
} from '@superset-ui/core';
import { SeriesOption } from 'echarts';
import {
collapseForecastKeys,
extractForecastSeriesContext,
extractForecastValuesFromTooltipParams,
formatForecastTooltipSeries,
@@ -411,3 +416,84 @@ test('formatForecastTooltipSeries should skip non-finite forecast values', () =>
}),
).toEqual(['<img>qwerty', '10']);
});
describe('formatForecastTooltipSeries truncation', () => {
const marker =
'<span style="display:inline-block;width:10px;height:10px;background-color:#1f77b4;"></span>';
const longName = 'prod-us-east-1-service-checkout-latency-p99'; // 43 chars
const intFormatter = getNumberFormatter(NumberFormats.INTEGER);
const format = (truncation?: 'off' | 'end' | 'start' | 'middle') =>
formatForecastTooltipSeries({
seriesName: longName,
observation: 1,
marker,
formatter: intFormatter,
...(truncation ? { truncation } : {}),
})[0];
test('leaves the name intact by default and for off/end', () => {
expect(format()).toContain(longName);
expect(format('off')).toContain(longName);
expect(format('end')).toContain(longName);
});
test('slices the start of the name without harming the marker', () => {
const cell = format('start');
expect(cell).toContain(marker);
expect(cell).toContain('…-us-east-1-service-checkout-latency-p99');
expect(cell).not.toContain('prod-us-east');
});
test('slices the middle of the name without harming the marker', () => {
const cell = format('middle');
expect(cell).toContain(marker);
expect(cell).toContain('prod-us-east-1-servi…heckout-latency-p99');
});
test('measures the budget against the name, not the marker markup', () => {
// The marker alone is far longer than the budget. If truncation were
// applied to the concatenated cell, a short name would be mangled.
expect(marker.length).toBeGreaterThan(TRUNCATION_MAX_CHARS);
const [cell] = formatForecastTooltipSeries({
seriesName: 'cpu',
observation: 1,
marker,
formatter: intFormatter,
truncation: 'start',
});
expect(cell).toBe(`${marker}cpu`);
});
});
describe('collapseForecastKeys', () => {
test('leaves plain observation series untouched and in order', () => {
expect(collapseForecastKeys(['foo', 'bar'])).toEqual(['foo', 'bar']);
});
test('folds a forecast bundle down to a single key', () => {
expect(
collapseForecastKeys([
'foo',
'foo__yhat',
'foo__yhat_lower',
'foo__yhat_upper',
]),
).toEqual(['foo']);
});
test('keeps a key for metrics whose labels are entirely forecast suffixes', () => {
// Charts can carry metrics literally labelled `ci__yhat*` with no plain
// observation series. Callers match these against forecast-stripped keys,
// so an uncollapsed id here would match nothing and drop every row.
expect(
collapseForecastKeys(['ci__yhat', 'ci__yhat_lower', 'ci__yhat_upper']),
).toEqual(['ci']);
});
test('preserves the incoming order of distinct series', () => {
expect(
collapseForecastKeys(['b__yhat_lower', 'a__yhat', 'b__yhat']),
).toEqual(['b', 'a']);
});
});
@@ -33,7 +33,7 @@
"mapbox-gl": "^3.28.1",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.2",
"supercluster": "^8.0.1"
"supercluster": "^9.0.0"
},
"peerDependencies": {
"@apache-superset/core": "*",
@@ -35,6 +35,43 @@ if (SERVICE_ACCOUNT_KEY.client_email) {
const DATETIME = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
/**
* Turn an oxlint diagnostic code into the canonical rule id used by the metrics
* series.
*
* oxlint reports `<plugin>(<rule>)`, where the plugin is the linter the rule came
* from: `eslint(no-console)`, `react-hooks(exhaustive-deps)`, `react(jsx-key)`,
* `jest(no-conditional-expect)`, `oxc(erasing-op)`, and the legacy
* `eslint-plugin-unicorn(no-new-array)` spelling.
*
* `eslint` is the implicit namespace, so its rules keep their bare name and stay
* comparable with the rows recorded before the oxlint migration. Every other
* plugin becomes `<plugin>/<rule>`, which is the id those rules are known by in
* config and in the pre-migration history.
*
* @param {string | undefined} code the diagnostic's `code` field
* @returns {string} the rule id to record
*/
function parseRuleId(code) {
if (!code) {
return 'unknown';
}
const match = code.match(/^([\w-]+)\(([^)]+)\)$/);
if (!match) {
return code;
}
const [, namespace, rule] = match;
if (namespace === 'eslint') {
return rule;
}
// `eslint-plugin-unicorn(...)` is the same rule as `unicorn/...`
const plugin = namespace.replace(/^eslint-plugin-/, '');
return `${plugin}/${rule}`;
}
async function writeToGoogleSheet(data, range, headers, append = false) {
if (!sheets) {
console.log('No Google Sheets credentials, skipping upload');
@@ -101,17 +138,7 @@ async function runOxlintAndProcess() {
// OXC JSON format has diagnostics array
if (results.diagnostics && Array.isArray(results.diagnostics)) {
results.diagnostics.forEach(diagnostic => {
// Extract rule ID from code like "eslint(no-unused-vars)" or "eslint-plugin-unicorn(no-new-array)"
const codeMatch = diagnostic.code?.match(
/^(?:eslint(?:-plugin-(\w+))?\()([^)]+)\)$/,
);
let ruleId = diagnostic.code || 'unknown';
if (codeMatch) {
const plugin = codeMatch[1];
const rule = codeMatch[2];
ruleId = plugin ? `${plugin}/${rule}` : rule;
}
const ruleId = parseRuleId(diagnostic.code);
const file = diagnostic.filename || 'unknown';
const line = diagnostic.labels?.[0]?.span?.line || 0;
@@ -251,5 +278,10 @@ async function runOxlintAndProcess() {
}
}
// Run the process
runOxlintAndProcess().catch(console.error);
// Run the process, unless this file was imported (e.g. by a test) rather than
// executed, in which case nothing should be linted or uploaded on import.
if (require.main === module) {
runOxlintAndProcess().catch(console.error);
}
module.exports = { parseRuleId };
@@ -164,7 +164,7 @@ export async function selectOption(option: string, selectName?: string) {
const item = await waitFor(() =>
within(
// eslint-disable-next-line testing-library/no-node-access
document.querySelector('.rc-virtual-list')!,
document.querySelector('.ant-select-dropdown-list')!,
).getByText(option),
);
await userEvent.click(item);
@@ -0,0 +1,58 @@
/**
* 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.
*/
const { parseRuleId } = require('../../scripts/oxlint-metrics-uploader');
test('eslint rules keep their bare name', () => {
expect(parseRuleId('eslint(no-console)')).toBe('no-console');
expect(parseRuleId('eslint(prefer-destructuring)')).toBe(
'prefer-destructuring',
);
});
test('plugin rules are recorded under plugin/rule (#42981)', () => {
// These are the codes oxlint actually emits. They previously fell through to
// the raw `react-hooks(exhaustive-deps)` string, so the rows no longer lined
// up with the ids the same rules were recorded under before the migration.
expect(parseRuleId('react-hooks(exhaustive-deps)')).toBe(
'react-hooks/exhaustive-deps',
);
expect(parseRuleId('react-hooks(rules-of-hooks)')).toBe(
'react-hooks/rules-of-hooks',
);
expect(parseRuleId('react(jsx-key)')).toBe('react/jsx-key');
expect(parseRuleId('jest(no-conditional-expect)')).toBe(
'jest/no-conditional-expect',
);
expect(parseRuleId('oxc(erasing-op)')).toBe('oxc/erasing-op');
expect(parseRuleId('typescript(no-explicit-any)')).toBe(
'typescript/no-explicit-any',
);
});
test('the legacy eslint-plugin- prefix still collapses to the plugin name', () => {
expect(parseRuleId('eslint-plugin-unicorn(no-new-array)')).toBe(
'unicorn/no-new-array',
);
});
test('an unrecognized or missing code is passed through rather than dropped', () => {
expect(parseRuleId('something-unparseable')).toBe('something-unparseable');
expect(parseRuleId(undefined)).toBe('unknown');
expect(parseRuleId('')).toBe('unknown');
});
@@ -390,7 +390,7 @@ const ResultSet = ({
// provides.
redirect(getExportCsvUrl(query.id));
},
confirmText: t('OK'),
confirmText: t('Confirm'),
cancelText: t('Close'),
});
}
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen } from 'spec/helpers/testing-library';
import { render, screen, userEvent } from 'spec/helpers/testing-library';
import SaveDatasetActionButton from 'src/SqlLab/components/SaveDatasetActionButton';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
@@ -27,6 +27,7 @@ describe('SaveDatasetActionButton', () => {
<SaveDatasetActionButton
setShowSave={() => true}
onSaveAsExplore={onSaveAsExplore}
canSaveDataset
/>,
);
@@ -41,4 +42,44 @@ describe('SaveDatasetActionButton', () => {
expect(saveBtn).toBeVisible();
expect(saveDatasetBtn).toBeVisible();
});
test('disables only the dataset button when canSaveDataset is false', () => {
const onSaveAsExplore = jest.fn();
render(
<SaveDatasetActionButton
setShowSave={() => true}
onSaveAsExplore={onSaveAsExplore}
canSaveDataset={false}
/>,
);
// Saving the query needs no results.
expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled();
expect(
screen.getByRole('button', { name: /save dataset/i }),
).toBeDisabled();
});
test('disables the save dataset button when the query did not run successfully', async () => {
render(
<SaveDatasetActionButton
setShowSave={() => true}
onSaveAsExplore={jest.fn()}
canSaveDataset={false}
/>,
);
const saveDatasetBtn = screen.getByRole('button', {
name: /save dataset/i,
});
expect(saveDatasetBtn).toBeDisabled();
// the disabled button is wrapped in a span so the tooltip still triggers
userEvent.hover(saveDatasetBtn.parentElement as HTMLElement);
expect(
await screen.findByRole('tooltip', {
name: 'You must run the query successfully first',
}),
).toBeInTheDocument();
});
});
@@ -23,11 +23,13 @@ import { Button } from '@superset-ui/core/components';
interface SaveDatasetActionButtonProps {
setShowSave: (arg0: boolean) => void;
onSaveAsExplore?: () => void;
canSaveDataset: boolean;
}
const SaveDatasetActionButton = ({
setShowSave,
onSaveAsExplore,
canSaveDataset,
}: SaveDatasetActionButtonProps) => (
<>
<Button
@@ -43,8 +45,13 @@ const SaveDatasetActionButton = ({
color="default"
variant="text"
onClick={() => onSaveAsExplore?.()}
disabled={!canSaveDataset}
icon={<Icons.TableOutlined />}
tooltip={t('Save or Overwrite Dataset')}
tooltip={
canSaveDataset
? t('Save or Overwrite Dataset')
: t('You must run the query successfully first')
}
aria-label={t('Save dataset')}
/>
)}
@@ -19,12 +19,14 @@
import { act, type ComponentProps } from 'react';
import {
cleanup,
createStore,
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'spec/helpers/testing-library';
import reducerIndex from 'spec/helpers/reducerIndex';
import fetchMock from 'fetch-mock';
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
import { createDatasource } from 'src/SqlLab/actions/sqlLab';
@@ -63,6 +65,12 @@ beforeEach(() => {
cleanup();
});
afterEach(() => {
// In-body restores are skipped when an assertion throws, leaking a
// configured spy into later tests.
jest.restoreAllMocks();
});
// 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
@@ -518,6 +526,39 @@ describe('SaveDatasetModal', () => {
});
});
test('surfaces the error and keeps the modal open when saving fails', async () => {
// The chart-payload step's toast was built but never dispatched, so a
// failure there was silent.
const postFormData = jest.spyOn(
require('src/explore/exploreUtils/formData'),
'postFormData',
);
postFormData.mockRejectedValue(new Error('Boom'));
const onHide = jest.fn();
const store = createStore({ user }, reducerIndex);
render(<SaveDatasetModal {...mockedProps} onHide={onHide} />, { store });
fireEvent.change(screen.getByDisplayValue(/unimportant/i), {
target: { value: 'my dataset' },
});
userEvent.click(screen.getByRole('button', { name: /save/i }));
// `createStore` builds its reducer map at runtime, so state isn't typed.
const toasts = () =>
(
store.getState() as unknown as {
messageToasts: { toastType: string }[];
}
).messageToasts;
await waitFor(() => {
expect(toasts()).toHaveLength(1);
});
expect(toasts()[0].toastType).toBe('DANGER_TOAST');
expect(onHide).not.toHaveBeenCalled();
});
test('clearDatasetCache is imported and available', () => {
const { clearDatasetCache } = require('src/utils/cachedSupersetGet');
@@ -61,6 +61,9 @@ import type Subject from 'src/types/Subject';
import { openInNewTab, redirect } from 'src/utils/navigationUtils';
import { mapSubjectValuesToIds } from 'src/features/subjects/SubjectPicker';
// Derived so it can't drift from what `getClientErrorObject` accepts.
type SaveErrorSource = Parameters<typeof getClientErrorObject>[0];
interface QueryDatabase {
id?: number;
}
@@ -391,9 +394,18 @@ export const SaveDatasetModal = ({
setDatasetName(getDefaultDatasetName());
onHide();
})
.catch(() => {
.catch((error?: SaveErrorSource) => {
setLoading(false);
addDangerToast(t('An error occurred saving dataset'));
// `createDatasource` already toasted the server's message and rejects
// with nothing; only the chart-payload step needs its own.
if (!error) {
return;
}
getClientErrorObject(error).then(e =>
dispatch(
addDangerToast(e.error || t('An error occurred saving dataset')),
),
);
});
};
@@ -27,6 +27,8 @@ import {
import SaveQuery from 'src/SqlLab/components/SaveQuery';
import { initialState, databases } from 'src/SqlLab/fixtures';
const RESULT_COLUMNS = [{ column_name: 'col', type: 'STRING' }];
const mockedProps = {
queryEditorId: '123',
animation: false,
@@ -59,8 +61,31 @@ const splitSaveBtnProps = {
...mockedProps.database,
allows_virtual_table_explore: true,
},
columns: RESULT_COLUMNS,
};
const EDITOR_SQL = 'SELECT * FROM t';
const stateWithLatestQuery = ({
id,
state,
sql = EDITOR_SQL,
}: {
id: string;
state: string;
sql?: string;
}) => ({
...mockState,
sqlLab: {
...mockState.sqlLab,
queryEditors: mockState.sqlLab.queryEditors.map(qe => ({
...qe,
latestQueryId: id,
})),
queries: { [id]: { id, state, sql } },
},
});
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
@@ -96,6 +121,71 @@ describe('SavedQuery', () => {
expect(saveBtn).toBeVisible();
});
test('blocks "Save dataset" until the query has run successfully', () => {
// Without a successful run the save can only fail server-side.
render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true,
store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'failed' })),
});
expect(
screen.getByRole('button', { name: /save dataset/i }),
).toBeDisabled();
// Saving the query itself is unaffected.
expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled();
});
test('blocks "Save dataset" when no query has been run at all', () => {
render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true,
store: mockStore(mockState),
});
expect(
screen.getByRole('button', { name: /save dataset/i }),
).toBeDisabled();
});
test('blocks "Save dataset" when the SQL changed after a successful run', () => {
// The run succeeded, but not for what is in the editor now -- and it is
// the editor's SQL that gets saved.
render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true,
store: mockStore(
stateWithLatestQuery({
id: 'qid-1',
state: 'success',
sql: 'SELECT 1 AS ran_earlier',
}),
),
});
expect(
screen.getByRole('button', { name: /save dataset/i }),
).toBeDisabled();
});
test('blocks "Save dataset" when the successful query returned no columns', () => {
// e.g. a DDL/DML statement -- there is nothing to introspect into a dataset.
render(<SaveQuery {...splitSaveBtnProps} columns={[]} />, {
useRedux: true,
store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })),
});
expect(
screen.getByRole('button', { name: /save dataset/i }),
).toBeDisabled();
});
test('enables "Save dataset" once the query has succeeded', () => {
render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true,
store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })),
});
expect(screen.getByRole('button', { name: /save dataset/i })).toBeEnabled();
});
test('renders a save query modal when user clicks save button', () => {
render(<SaveQuery {...mockedProps} />, {
useRedux: true,
@@ -233,7 +323,7 @@ describe('SavedQuery', () => {
test('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true,
store: mockStore(mockState),
store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })),
});
const saveDatasetMenuItem = await screen.findByLabelText(/save dataset/i);
@@ -247,7 +337,7 @@ describe('SavedQuery', () => {
test('renders the save dataset modal UI', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true,
store: mockStore(mockState),
store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })),
});
const saveDatasetMenuItem = await screen.findByLabelText(/save dataset/i);
userEvent.click(saveDatasetMenuItem);
@@ -17,6 +17,8 @@
* under the License.
*/
import { useState, useEffect, useMemo, ChangeEvent } from 'react';
import { useSelector } from 'react-redux';
import { Query, QueryState } from '@superset-ui/core';
import type { DatabaseObject } from 'src/features/databases/types';
import { t } from '@apache-superset/core/translation';
import { styled } from '@apache-superset/core/theme';
@@ -37,7 +39,7 @@ import {
} from 'src/SqlLab/components/SaveDatasetModal';
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
import { QueryEditor } from 'src/SqlLab/types';
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
import useLogAction from 'src/logger/useLogAction';
import {
LOG_ACTIONS_SQLLAB_CREATE_CHART,
@@ -111,6 +113,17 @@ const SaveQuery = ({
const [label, setLabel] = useState<string>(defaultLabel);
const [showSave, setShowSave] = useState<boolean>(false);
const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
// Saving a dataset runs the SQL to introspect columns, so it needs a
// successful run of the SQL being saved that produced at least one column
// -- editing after a run invalidates it, and running a selection only
// validates that selection.
const latestQuery = useSelector<SqlLabRootState, Query | undefined>(
({ sqlLab }) => sqlLab.queries[queryEditor.latestQueryId || ''],
);
const canSaveDataset =
latestQuery?.state === QueryState.Success &&
latestQuery.sql === queryEditor.sql &&
columns.length > 0;
const isSaved = !!query.remoteId;
const isLabelEmpty = label.trim().length === 0;
const canExploreDatabase = !!database?.allows_virtual_table_explore;
@@ -207,6 +220,7 @@ const SaveQuery = ({
<SaveDatasetActionButton
setShowSave={setShowSave}
onSaveAsExplore={canExploreDatabase ? onSaveAsExplore : undefined}
canSaveDataset={canSaveDataset}
/>
)}
<SaveDatasetModal
@@ -21,6 +21,7 @@ import {
isFeatureEnabled,
getExtensionsRegistry,
FeatureFlag,
QueryState,
} from '@superset-ui/core';
import {
act,
@@ -334,6 +335,50 @@ describe('SqlEditor', () => {
expect(await findByText('10 000')).toBeInTheDocument();
});
const setupWithLatestQuery = (overrides: Partial<typeof latestQuery>) =>
setup(
mockedProps,
createStore({
...mockInitialState,
sqlLab: {
...mockInitialState.sqlLab,
queries: {
[latestQuery.id]: { ...latestQuery, ...overrides },
},
databases: {
1991: {
...mockInitialState.sqlLab.databases[1991],
allows_virtual_table_explore: true,
},
},
},
}),
);
test('enables the save dataset button when the latest query succeeded', async () => {
const { findByRole } = setupWithLatestQuery({
state: QueryState.Success,
sql: mockedProps.queryEditor.sql,
});
expect(await findByRole('button', { name: 'Save dataset' })).toBeEnabled();
});
test('disables the save dataset button when the latest query failed', async () => {
const { findByRole } = setupWithLatestQuery({
state: QueryState.Failed,
results: undefined,
});
expect(await findByRole('button', { name: 'Save dataset' })).toBeDisabled();
});
test('disables the save dataset button when the results are not loaded', async () => {
const { findByRole } = setupWithLatestQuery({
state: QueryState.Success,
results: undefined,
});
expect(await findByRole('button', { name: 'Save dataset' })).toBeDisabled();
});
test('renders an Extension if provided', async () => {
const extensionsRegistry = getExtensionsRegistry();
@@ -40,6 +40,7 @@ import {
getExtensionsRegistry,
QueryResponse,
Query,
QueryState,
} from '@superset-ui/core';
import { Alert } from '@apache-superset/core/components';
import { css, styled, useTheme } from '@apache-superset/core/theme';
@@ -295,6 +296,9 @@ const SqlEditor: FC<Props> = ({
const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form');
const successful = latestQuery?.state === QueryState.Success;
const resultColumns = latestQuery?.results?.columns || [];
const startQuery = useCallback(
(
ctasArg = false,
@@ -712,7 +716,6 @@ const SqlEditor: FC<Props> = ({
const getSecondaryMenuItems = () => {
const qe = queryEditor;
const successful = latestQuery?.state === 'success';
const scheduleToolTip = successful
? t('Schedule the query periodically')
: t('You must run the query successfully first');
@@ -858,7 +861,7 @@ const SqlEditor: FC<Props> = ({
)}
<SaveQuery
queryEditorId={queryEditor.id}
columns={latestQuery?.results?.columns || []}
columns={resultColumns}
onSave={onSaveQuery}
onUpdate={(query, remoteId) =>
dispatch(updateSavedQuery(query, remoteId))
@@ -223,6 +223,11 @@ export default function chartReducer(
}
if (action.type in actionHandlers) {
// ADD_CHART creates the entry, so it runs without prior state; every other
// handler reads state that is absent once the chart has been removed
if (action.type !== actions.ADD_CHART && !charts[action.key]) {
return charts;
}
return {
...charts,
[action.key]: actionHandlers[action.type](charts[action.key]),
@@ -91,4 +91,20 @@ describe('chart reducers', () => {
expect(newState[chartKey].chartUpdateEndTime).toBeGreaterThan(0);
expect(newState[chartKey].chartStatus).toEqual('failed');
});
test('ignores an action for a chart that is no longer in state', () => {
const action = actions.chartUpdateStopped(999, new AbortController());
expect(() => chartReducer(charts, action)).not.toThrow();
expect(chartReducer(charts, action)).toEqual(charts);
});
test('still adds a chart that is not yet in state', () => {
const newChartKey = 2;
const newState = chartReducer(
charts,
actions.addChart({ ...chart, id: newChartKey }, newChartKey),
);
expect(newState[newChartKey].id).toEqual(newChartKey);
expect(newState[chartKey]).toEqual(testChart);
});
});
@@ -17,6 +17,7 @@
* under the License.
*/
import { useState } from 'react';
import fetchMock from 'fetch-mock';
import {
cleanup,
render,
@@ -35,6 +36,10 @@ import { useDrillDetailMenuItems, DrillDetailMenuItemsProps } from './index';
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
// Opening the context menu logs an event, and an unmatched request makes
// fetch-mock throw inside the component.
fetchMock.post('glob:*/log/?*', {});
jest.mock(
'../DrillDetail/DrillDetailPane',
() =>
@@ -120,7 +120,7 @@ describe('DatasourceModal', () => {
});
const saveButton = screen.getByTestId('datasource-modal-save');
fireEvent.click(saveButton);
const okButton = await screen.findByRole('button', { name: 'OK' });
const okButton = await screen.findByRole('button', { name: 'Confirm' });
fireEvent.click(okButton);
await waitFor(() => {
expect(onDatasourceSave).toHaveBeenCalled();
@@ -142,7 +142,7 @@ describe('DatasourceModal', () => {
const saveButton = screen.getByTestId('datasource-modal-save');
fireEvent.click(saveButton);
const okButton = await screen.findByRole('button', { name: 'OK' });
const okButton = await screen.findByRole('button', { name: 'Confirm' });
fireEvent.click(okButton);
const errorElements = await screen.findAllByText('Error saving dataset');
@@ -230,7 +230,7 @@ describe('DatasourceModal', () => {
expect(checkbox).toBeChecked();
// Click OK to submit
const okButton = screen.getByRole('button', { name: 'OK' });
const okButton = screen.getByRole('button', { name: 'Confirm' });
fireEvent.click(okButton);
// Verify the PUT request was made with override_columns=true
@@ -297,7 +297,7 @@ describe('DatasourceModal', () => {
expect(checkbox).not.toBeChecked();
// Click OK to submit
const okButton = screen.getByRole('button', { name: 'OK' });
const okButton = screen.getByRole('button', { name: 'Confirm' });
fireEvent.click(okButton);
// Verify the PUT request was made with override_columns=false
@@ -395,7 +395,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
show={confirmModalOpen}
onHide={handleConfirmModalClose}
onHandledPrimaryAction={handleConfirmSave}
primaryButtonName={t('OK')}
primaryButtonName={t('Confirm')}
primaryButtonLoading={isSaving}
>
{getSaveDialog()}
@@ -867,7 +867,7 @@ function DatasourceEditor({
return {
...metric,
certification_details: certificationDetails || details,
warning_markdown: warningMarkdown || '',
warning_markdown: warningMarkdown || metric.warning_markdown || '',
certified_by: certifiedBy || certifiedByMetric,
};
}),
@@ -1627,9 +1627,7 @@ function DatasourceEditor({
{t(
'Default URL to redirect to when accessing from the dataset list page. Accepts relative URLs such as',
)}{' '}
<Typography.Text code>
/superset/dashboard/{'{id}'}/
</Typography.Text>
<Typography.Text code>/dashboard/{'{id}'}/</Typography.Text>
</>
}
control={<TextControl controlId="default_endpoint" />}
@@ -71,6 +71,17 @@ test('renders Tabs', async () => {
expect(screen.getByTestId('edit-dataset-tabs')).toBeInTheDocument();
});
test('recommends a registered client route for the default URL', async () => {
await asyncRender(createProps());
userEvent.click(screen.getByRole('tab', { name: 'Settings' }));
expect(await screen.findByText('/dashboard/{id}/')).toBeInTheDocument();
expect(
screen.queryByText('/superset/dashboard/{id}/'),
).not.toBeInTheDocument();
});
test('can sync columns from source', async () => {
const testProps = createProps();
await asyncRender({
@@ -0,0 +1,73 @@
/**
* 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 fetchMock from 'fetch-mock';
import { screen, userEvent, waitFor } from 'spec/helpers/testing-library';
import {
createProps,
DATASOURCE_ENDPOINT,
setupDatasourceEditorMocks,
cleanupAsyncOperations,
fastRender,
dismissDatasourceWarning,
} from './DatasourceEditor.test.utils';
beforeEach(() => {
fetchMock.get(DATASOURCE_ENDPOINT, [], { name: DATASOURCE_ENDPOINT });
setupDatasourceEditorMocks();
});
afterEach(async () => {
await cleanupAsyncOperations();
fetchMock.clearHistory().removeRoutes();
});
// Certifying a metric fills two adjacent fields in one visit to the expanded
// row. Both are committed through TextControl's debounce, so the second one
// used to land on the item as it looked before the first had been applied,
// leaving the saved metric with details but no certifier.
test('certifying a metric keeps both certified_by and certification_details', async () => {
const testProps = createProps();
fastRender(testProps);
await dismissDatasourceWarning();
await userEvent.click(await screen.findByTestId('collection-tab-Metrics'));
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
await userEvent.type(
await screen.findByPlaceholderText('Certified by'),
'Metric Certifier',
);
await userEvent.type(
await screen.findByPlaceholderText('Certification details'),
'Metric cert details',
);
await waitFor(() => {
const { calls } = testProps.onChange.mock;
const savedMetrics = calls[calls.length - 1]?.[0]?.metrics ?? [];
const saved = savedMetrics.find(metric => metric.metric_name === 'count');
expect(saved).toEqual(
expect.objectContaining({
certified_by: 'Metric Certifier',
certification_details: 'Metric cert details',
}),
);
});
});
@@ -0,0 +1,97 @@
/**
* 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 fetchMock from 'fetch-mock';
import { screen, userEvent } from 'spec/helpers/testing-library';
import {
createProps,
DATASOURCE_ENDPOINT,
setupDatasourceEditorMocks,
cleanupAsyncOperations,
fastRender,
dismissDatasourceWarning,
} from './DatasourceEditor.test.utils';
// Stub the Ace-backed control with a plain textarea. Ace spreads its document
// across many spans and keeps only the keystroke buffer in its own textarea,
// so asserting on the value the control receives is less brittle than
// reaching into Ace's DOM.
jest.mock('src/explore/components/controls/TextAreaControl', () => ({
__esModule: true,
default: ({
controlId,
value,
onChange,
}: {
controlId?: string;
value?: string;
onChange?: (value: string) => void;
}) => (
<textarea
data-test={`mock-textarea-${controlId}`}
value={value ?? ''}
onChange={event => onChange?.(event.target.value)}
/>
),
}));
beforeEach(() => {
fetchMock.get(DATASOURCE_ENDPOINT, [], { name: DATASOURCE_ENDPOINT });
setupDatasourceEditorMocks();
});
afterEach(async () => {
await cleanupAsyncOperations();
fetchMock.clearHistory().removeRoutes();
});
// Regression test for #42704. Explore's datasource payload (SqlMetric.data on
// the backend) exposes warning_markdown as a flattened top-level field and
// omits the raw `extra` JSON string that the /api/v1/dataset/{id} endpoint
// backing the Datasets page provides. Deriving warning_markdown purely from
// `extra` therefore dropped the saved text when the modal was opened from
// Explore, leaving the Warning field blank on reopen.
test('keeps a pre-existing top-level warning_markdown when the metric has no extra', async () => {
const baseProps = createProps();
const testProps = {
...baseProps,
datasource: {
...baseProps.datasource,
metrics: [
{
...baseProps.datasource.metrics[0],
warning_markdown: 'existing warning',
extra: undefined,
},
],
},
};
fastRender(testProps);
await dismissDatasourceWarning();
const metricsTab = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricsTab);
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
expect(
await screen.findByTestId('mock-textarea-warning_markdown'),
).toHaveValue('existing warning');
});
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode, useCallback } from 'react';
import { ReactNode, useCallback, useEffect, useRef } from 'react';
import { Divider, Form, Typography } from '@superset-ui/core/components';
import { css } from '@apache-superset/core/theme';
import { recurseReactClone } from '../../utils';
@@ -39,14 +39,24 @@ export default function Fieldset({
title = null,
compact = false,
}: FieldsetProps) {
// Controls report their edits asynchronously - TextControl debounces by
// FAST_DEBOUNCE - so the callback that eventually fires was built during an
// earlier render. Spreading that render's `item` rebuilds the whole record
// from a snapshot taken before a sibling field committed, dropping the value
// the user typed first. Reading off a ref merges into the latest commit.
const itemRef = useRef(item);
useEffect(() => {
itemRef.current = item;
}, [item]);
const handleChange = useCallback(
(fieldKey: fieldKeyType, val: any) => {
onChange?.({
...item,
...itemRef.current,
[fieldKey]: val,
});
},
[onChange, item],
[onChange],
);
const propExtender = (field: { props: { fieldKey: fieldKeyType } }) => ({
@@ -135,6 +135,15 @@ describe('dashboardState actions', () => {
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('saveDashboardRequest', () => {
const findDangerToast = (dispatch: jest.Mock) =>
dispatch.mock.calls
.map(call => call[0])
.find(
action =>
action?.type === ADD_TOAST &&
action.payload.toastType === ToastType.Danger,
);
test('should dispatch UPDATE_COMPONENTS_PARENTS_LIST action', () => {
const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false },
@@ -227,6 +236,89 @@ describe('dashboardState actions', () => {
const { body } = putStub.mock.calls[0][0];
expect(body).toBe(JSON.stringify(confirmedDashboardData));
});
test('warns about the overwrite values when a diff is detected', async () => {
const { getState, dispatch } = setup();
const thunk = saveDashboardRequest(
newDashboardData,
192,
SAVE_TYPE_OVERWRITE,
);
thunk(dispatch, getState);
await waitFor(() =>
expect(findDangerToast(dispatch)?.payload.text).toBe(
'Please confirm the overwrite values.',
),
);
expect(putStub.mock.calls.length).toBe(0);
});
test('reports the actual error when the overwrite precheck fails', async () => {
getStub.mockRestore();
getStub = jest
.spyOn(SupersetClient, 'get')
.mockRejectedValue(new Error('precheck exploded'));
const { getState, dispatch } = setup();
const thunk = saveDashboardRequest(
newDashboardData,
192,
SAVE_TYPE_OVERWRITE,
);
thunk(dispatch, getState);
await waitFor(() =>
expect(findDangerToast(dispatch)?.payload.text).toContain(
'precheck exploded',
),
);
expect(putStub.mock.calls.length).toBe(0);
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('when FeatureFlag.CONFIRM_DASHBOARD_DIFF is disabled', () => {
beforeEach(() => {
mockIsFeatureEnabled.mockImplementation(() => false);
});
afterEach(() => {
mockIsFeatureEnabled.mockRestore();
});
test('never runs the overwrite precheck', async () => {
const { getState, dispatch } = setup();
const thunk = saveDashboardRequest(
newDashboardData,
192,
SAVE_TYPE_OVERWRITE,
);
thunk(dispatch, getState);
await waitFor(() => expect(putStub.mock.calls.length).toBe(1));
expect(getStub).not.toHaveBeenCalledWith(
expect.objectContaining({ endpoint: '/api/v1/dashboard/192' }),
);
});
// An unexpected failure used to reach the overwrite-confirm handler,
// which reported it as "Please confirm the overwrite values." even with
// the feature flag off, hiding the real error.
test('reports the actual error when the update throws unexpectedly', async () => {
putStub.mockRestore();
putStub = jest.spyOn(SupersetClient, 'put').mockImplementation(() => {
throw new Error('unexpected boom');
});
const { getState, dispatch } = setup();
const thunk = saveDashboardRequest(
newDashboardData,
192,
SAVE_TYPE_OVERWRITE,
);
thunk(dispatch, getState);
await waitFor(() =>
expect(findDangerToast(dispatch)?.payload.text).toContain(
'unexpected boom',
),
);
});
});
test('should navigate to the new dashboard after Save As', async () => {
@@ -379,15 +471,6 @@ describe('dashboardState actions', () => {
// permission-denied copy, while a 403 from outside Superset (reverse proxy,
// WAF, SSO gateway) carries a non-JSON body and must fall back to the
// generic status-derived toast. See #42239.
const findDangerToast = (dispatch: jest.Mock) =>
dispatch.mock.calls
.map(call => call[0])
.find(
action =>
action?.type === ADD_TOAST &&
action.payload.toastType === ToastType.Danger,
);
test('maps a non-JSON 403 save failure to the generic error toast', async () => {
const { getState, dispatch } = setup();
putStub.mockRestore();

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