Compare commits

...
Author SHA1 Message Date
Beto DealmeidaandClaude Opus 4.7 7716466704 Address review + reconcile with time-grain PR
Reconcile with #41456: that PR sets ``expression=None`` on SV
dimensions so a truthy ``expression`` (the "adhoc SQL column" signal)
does not hide the time-grain selector. As a consequence, SV
dimensions with empty ``expression`` land in the popover's Simple
list, not the Saved list — the split in ``ColumnSelectPopover.tsx``
is done purely on ``expression`` truthiness. Disabling Simple would
leave dimensions unreachable in the popover, so only Custom SQL
stays disabled.

- Drop ``'simple'`` from ``effectiveDisabledTabs`` in
  ``DndColumnSelect.tsx`` and ``DndColumnMetricSelect.tsx``; keep
  ``'sqlExpression'``. Update comments to explain the tie to #41456.
- Rewrite the popover test to assert the new SV behavior (Simple
  enabled and selected by default; Custom SQL disabled; Saved
  enabled but not initial). Drop the ``as any`` cast per review.
- Remove PR-added "Saved tab" comment blocks in ``models.py`` — they
  will be misleading once #41456 lands with its own ``expression``
  rationale.
- Remove the four ``expression == "..."`` assertions in
  ``models_test.py`` for the same reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-07 09:12:13 -04:00
Beto Dealmeida 13d94b2d23 fix(semantic layers): disable Simple tab in columns 2026-07-07 09:12:13 -04:00
Beto Dealmeida 4dc45aa958 fix(semantic layers): apply post-processing (#41601) 2026-07-07 08:52:44 -04:00
Beto Dealmeida 4167a21f7d fix(semantic layers): guards for zero rows (#41600) 2026-07-07 08:51:45 -04:00
5e384f54d3 fix(ag-grid): select cell on click instead of its text (#41392)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com>
2026-07-07 10:35:13 +02:00
anamitraadhikariandaadhikari ec4e5deb01 feat(theming): add opt-in SQL Lab grid tokens and fix modal border-radius clipping (#41031)
Co-authored-by: aadhikari <aadhikari@apple.com>
2026-07-07 09:54:58 +02:00
Mehmet Salih Yavuz de5a233ccf fix(sqllab): preserve saved query description when editing (#41685) 2026-07-07 09:19:25 +02:00
c846da8dfb fix(frontend): resolve image download formatting and cleanup issues (#38927)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:54:26 -07:00
1c3b070d34 feat(i18n): do-not-translate registry + '#. do-not-translate' marker standard (#41651)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:31:41 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>EvanClaude Opus 4.8
a8a1b220fd chore(deps): update @ant-design/icons requirement from ^6.2.5 to ^6.3.2 in /superset-frontend/packages/superset-ui-core (#41790)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:31:19 -07:00
Evan RusackasandClaude Code e52240b9a6 test(datasets): prove column metadata persists for zero-row queries (#41838)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:49:18 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 87fa7e5e64 chore(deps-dev): bump prettier from 3.9.1 to 3.9.3 in /superset-websocket (#41810)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 08:45:20 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2b74e140f3 chore(deps-dev): update sqlalchemy-parseable requirement from <0.2.0,>=0.1.3 to >=0.1.6,<0.2.0 (#41814)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 08:44:49 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 847bdad960 chore(deps-dev): bump @typescript-eslint/parser from 8.62.0 to 8.62.1 in /superset-frontend (#41820)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 08:42:44 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4b78758fd2 chore(deps-dev): bump typescript-eslint from 8.62.0 to 8.62.1 in /superset-websocket (#41815)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 08:42:15 +07:00
Evan RusackasandClaude Fable 5 101c1e8827 fix(ace-editor): carry token classes onto emoji boxes (#41821)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 18:06:01 -07:00
Evan RusackasandClaude Code 85eaf15c5e test(databases): stabilize flaky SQLAlchemy-form visibility assertion (#41823)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-06 18:05:38 -07:00
Evan RusackasandClaude Code 2aa43f6f0f fix(sql): stop sanitize_clause from rewriting user SQL semantics (#36113) (#41125)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-06 18:03:26 -07:00
Elizabeth ThompsonandClaude Sonnet 4.6 ee524b3471 fix(dashboard): let CSV exports use query cache instead of always force-querying (#41469)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 16:50:06 -07:00
luca-vercelliandClaude Opus 4.8 9e50d9eca6 fix: Italian translation (#36114)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:21:20 -07:00
033b7bc385 fix(databases): update broken database documentation links to /user-docs namespace (#41557)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-06 13:02:02 -07:00
yousophandClaude Opus 4.8 c0b0a2fdaf feat(tags): add favorites filter to Tags list view (#41461)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 12:55:01 -07:00
Ville Brofeldt 44c1ad5c71 fix: delint frontend files (#41802) 2026-07-06 21:54:33 +02:00
fb496e158a fix(ag-grid): persist "None" value aggregation selection (#41386)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:51:26 +02:00
Đỗ Trọng HảiandClaude Code 69ce27f173 feat(sec): resolve all vuln alerts in superset-embedded-sdk, superset-frontend and docs (#41776)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-06 12:14:34 -07:00
Richard Fogaca Nienkotter 51e708da6b fix(mcp): accept Superset vocabulary in chart configs and clarify query_dataset metric errors (#40972) 2026-07-06 16:01:14 -03:00
21b4185819 fix(ace-editor): caret drift from late font loads and emoji widths (#41697)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-07-06 11:54:11 -07:00
Đỗ Trọng Hải 689b4f623e docs(admin/config): resolve misconfigured aws-iam Markdown file (#41748)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-06 11:51:15 -07:00
Đỗ Trọng Hải ad8e6d4140 build(deps): upgrade react-markdown to v9 and remark-gfm to v4 (#41778)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-06 11:50:09 -07:00
marunandEvan Rusackas 83d93b8b42 fix(view-query): Add ownState parameter to ViewQueryModal for query consistency (#35208)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-06 11:16:55 -07:00
Elizabeth Thompson 06f421ed4a fix(a11y): add aria-label to remove-sheet icon button in Google Sheets catalog (#41796) 2026-07-06 10:38:42 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dc671ae44a chore(deps-dev): bump eslint from 10.5.0 to 10.6.0 in /superset-frontend (#41792)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 10:38:16 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c49d01223c chore(deps-dev): bump webpack from 5.108.0 to 5.108.2 in /docs (#41786)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 10:37:37 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 69f052985f chore(deps-dev): bump prettier from 3.8.4 to 3.9.3 in /superset-frontend (#41784)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 10:37:23 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f4cc871f8c chore(deps): bump js-yaml from 5.1.0 to 5.2.0 in /docs (#41783)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 10:37:09 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2f11240339 chore(deps): bump query-string from 9.4.0 to 9.4.1 in /superset-frontend (#41782)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 10:36:55 -07:00
a7a051777c feat(charts): soft-delete and restore (#40129)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-06 10:07:04 -07:00
Enzo Martellucci b4218136be feat(ag-grid): support Show summary in raw records mode (#41754) 2026-07-06 17:58:02 +02:00
Joe LiandClaude Opus 4.8 fd8d7009a5 chore(mcp): drop legacy /superset prefix from dashboard URLs (#41758)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 08:57:08 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> beff76cb4d chore(deps): bump antd from 6.4.5 to 6.5.0 in /docs (#41788)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 21:27:14 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d6b31376a5 chore(deps-dev): bump prettier from 3.8.4 to 3.9.1 in /docs (#41789)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 21:21:11 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1575b83f96 chore(deps): bump react-arborist from 3.10.5 to 3.12.0 in /superset-frontend (#41793)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 21:19:50 +07:00
83826839a0 fix(i18n): inject language pack into bootstrap to translate code-split chunks (#39357)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-05 21:15:40 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Evan Rusackas
a8ab5f973d chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.61.1 to 8.62.0 in /superset-frontend (#41577)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-05 15:55:31 -07:00
Elizabeth ThompsonandĐỗ Trọng Hải 7cdfe3ffe7 fix(a11y): add aria-label to CRON schedule input (#41769)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-07-05 15:01:00 -07:00
9a24d42d65 fix(chart): Fix heatmap charts when filtering down to one row/colum (#40738)
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-05 14:24:33 -07:00
3303d4e09e chore(deps): bump lodash-es to ^4.18.1 across all frontend workspaces (#41774)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-07-05 21:28:58 +07:00
haydenanddevops-sewhan-kim 8229c0183d perf(explore): reuse chart query data in Results tab (#38165)
Co-authored-by: devops-sewhan-kim <sewhan.kim@a-bly.com>
2026-07-04 23:27:57 -07:00
Viktor Högberg ea40b87e2f fix(drill-detail): drill to detail by a null value dimension should use NULL_STRING (#41678) 2026-07-04 23:18:07 -07:00
innovark dc739e4fb9 fix(ui): standardize card kebab menu behavior (#41741) 2026-07-04 23:15:33 -07:00
RISHAV DEWAN a687e87b2c fix(dashboard): use theme colorBorder token for chart tile borders (#41731) 2026-07-04 23:11:51 -07:00
Durgaprasad M L d91c5cf09b fix(dashboard): prevent dashboard crash when applying grain filters (#41700) (#41746) 2026-07-04 23:07:11 -07:00
Durgaprasad M L 3ac19470ec fix(embedded): register AG Grid modules for view-as-table modal (#41632) 2026-07-04 23:06:13 -07:00
Đỗ Trọng Hải 687fafd424 build(dev-deps): replace deprecated minimizer deps with recommendation from Webpack doc (#41756) 2026-07-05 08:58:34 +07:00
Beto DealmeidaandJoe Li 8fcc0f8b48 fix(semantic layers): start/end ranges (#41590)
Co-authored-by: Joe Li <joe@preset.io>
2026-07-04 16:46:26 -07:00
e08c2c12da fix(native-filters): persist created/pasted default values in select filter (#40984)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-04 16:44:29 -07:00
Joe LiandClaude Opus 4.7 1e50316bcc chore(reports): deprecate Slack v1 and harden Slack v2 tests (#39914)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-04 16:41:39 -07:00
Elizabeth Thompson 3e152d9bb7 fix(views): add new_target to deprecated explore_json_data endpoint (#41771) 2026-07-04 15:01:42 -07:00
Pawanandjaymasiwal 004c401c97 refactor(dashboard): rename supersetCanCSV to supersetCanDownload (#24290) (#39118)
Co-authored-by: jaymasiwal <jaymasiwal@users.noreply.github.com>
2026-07-04 13:06:14 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a9aabdaedf chore(deps): bump gunicorn from 25.3.0 to 26.0.0 (#41761)
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-07-04 10:03:16 -07:00
Đỗ Trọng Hải 210389478b fix(test): change regex for RTL to retrieve button used for filter removal (#41767)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-04 09:58:30 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 43f2816240 chore(deps-dev): update ydb-sqlglot-plugin requirement from >=0.2.5 to >=0.2.8 (#41764)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-04 15:04:59 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c3fe0a40eb chore(deps-dev): bump mysqlclient from 2.2.6 to 2.2.8 (#41760)
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-07-04 14:53:22 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
71ac6c64e2 chore(deps): bump rison from 2.0.0 to 2.0.1 (#41762)
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-07-04 14:52:53 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 81a437826f chore(deps-dev): update kylinpy requirement from <2.9,>=2.8.1 to >=2.8.4,<2.9 (#41765)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-04 14:52:36 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fd1f313b30 chore(deps): bump flask-appbuilder from 5.2.1 to 5.2.2 (#41766)
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-07-04 14:36:33 +07:00
b23cef136e fix(i18n): un-translate do-not-translate tokens in es/fi/th catalogs (#41652)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:37:33 -07:00
Beto Dealmeida 681275077b fix(semantic layers): time comparison with 1 row (#41554) 2026-07-03 14:39:15 -07:00
d7ad7fbb49 fix(i18n): repair dropped placeholders and reduced-plural forms across catalogs (#41722)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:33:26 -07:00
59d35d16ce feat(i18n): adopt French translation improvements from #41688 (#41752)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Jean Pommier <jean.pommier@pi-geosolutions.fr>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:07:27 -07:00
4e2160079a feat(i18n): backfill Arabic (ar) translations (AI-generated, needs review) (#41705)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:40:35 -07:00
ceeba01305 fix(i18n): correct mistranslated entries flagged in review (pl/pt_BR/sl/sk/fi) (#41707)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:40:18 -07:00
6d7344750f feat(i18n): backfill Chinese (Simplified) (zh) translations (AI-generated, needs review) (#41708)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:40:01 -07:00
7d90684f93 feat(i18n): backfill Chinese (Traditional) (zh_TW) translations (AI-generated, needs review) (#41709)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:39:44 -07:00
e7b825e26b feat(i18n): backfill Portuguese (pt) translations (AI-generated, needs review) (#41710)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:39:27 -07:00
b6f8267ed2 feat(i18n): backfill Italian (it) translations (AI-generated, needs review) (#41712)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:39:10 -07:00
d443dd17b9 feat(i18n): backfill Korean (ko) translations (AI-generated, needs review) (#41713)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:37:50 -07:00
Elizabeth Thompson 2702113d99 fix(a11y): add aria-label to VizTypeGallery search clear icon (#41681) 2026-07-03 10:39:20 -07:00
Elizabeth Thompson 0b14f1c226 fix(a11y): use aria-label instead of non-functional alt prop on filter icons (#41742) 2026-07-03 10:39:04 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Đỗ Trọng Hảihainenber
114c258145 chore(deps-dev): bump webpack from 5.107.2 to 5.108.0 in /superset-frontend (#41733)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-07-03 23:58:39 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9f8ff1e87f chore(deps): bump @deck.gl/mapbox from 9.3.4 to 9.3.5 in /superset-frontend (#41735)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-03 21:15:13 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2b30605e3c chore(deps): bump baseline-browser-mapping from 2.10.38 to 2.10.40 in /docs (#41736)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-03 21:14:41 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d93098f853 chore(deps-dev): bump baseline-browser-mapping from 2.10.38 to 2.10.40 in /superset-frontend (#41737)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-03 21:14:06 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> eaf6daa7eb chore(deps-dev): bump webpack from 5.107.2 to 5.108.0 in /docs (#41734)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-03 21:11:57 +07:00
0e6c5838e4 fix(deck.gl): apply categorical scatterplot colors in Multiple Layers chart (#41490)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com>
2026-07-03 14:23:29 +02:00
4f37e955b5 fix(plugin-chart-echarts): prevent trendline stroke clipping at chart edges (#37918)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-03 10:59:13 +02:00
46a153d17e feat(i18n): backfill Persian (Farsi) (fa) translations (AI-generated, needs review) (#41701)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:58:21 -07:00
6d22697cba feat(theming): make core components fully configurable via theme tokens (#40985)
Co-authored-by: aadhikari <aadhikari@apple.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-07-02 17:55:48 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7cbdc726e3 chore(deps-dev): bump @storybook/addon-links from 10.4.4 to 10.4.6 in /superset-frontend (#41366)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 17:54:54 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ceadce234b chore(deps-dev): bump @storybook/react-webpack5 from 10.4.4 to 10.4.6 in /superset-frontend (#41371)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 17:34:53 -07:00
47bc3e2dc1 fix: correct Security menu case for MySQL deployments (#40527)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-02 17:23:31 -07:00
49fcaf2420 feat(i18n): backfill Turkish (tr) translations (AI-generated, needs review) (#41702)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:21:17 -07:00
Enzo MartellucciandClaude Opus 4.7 55088e10da feat(database-modal): add validation loading state and duplicate name check (#36880)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-02 16:52:13 -07:00
Mike BridgeandMike Bridge bdc610c572 fix(db): use a private engine for prequery connections to avoid listener race (#41642)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-07-02 16:30:09 -07:00
a151edeff3 feat(i18n): backfill Dutch (nl) translations (AI-generated, needs review) (#41704)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:27:24 -07:00
Elizabeth Thompson d8832c382d fix(views): emit deprecated-endpoint log warning once per endpoint per process (#41286) 2026-07-02 16:10:37 -07:00
Elizabeth Thompson 5bbab86a07 fix(schemas): rename deprecated query fields regardless of falsy values (#41263) 2026-07-02 16:10:10 -07:00
ab0e77c1cb fix(embedded): allow guest users to sort by visible columns (#37371)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:50:56 -07:00
Joe Li a30846881b test(ci): stabilize master checks (#41650) 2026-07-02 15:32:06 -07:00
Elizabeth Thompson 22c3f56d0a fix(redshift): suppress unavoidable pkg_resources deprecation warning (#41691) 2026-07-02 15:01:23 -07:00
66bf81b997 feat(i18n): backfill Russian (ru) translations (AI-generated, needs review) (#41649)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:59:15 -07:00
03703843b7 feat(i18n): backfill French (fr) translations (AI-generated, needs review) (#41655)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:57:58 -07:00
f61d6d8b84 feat(i18n): backfill Māori (mi) translations (AI-generated, needs review) (#41656)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:57:32 -07:00
7eb93c60a3 feat(i18n): backfill Catalan (ca) translations (AI-generated, needs review) (#41657)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:57:19 -07:00
9e08770291 feat(i18n): backfill Slovenian (sl) translations (AI-generated, needs review) (#41658)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:57:05 -07:00
358493a2c5 feat(i18n): backfill Brazilian Portuguese (pt_BR) translations (AI-generated, needs review) (#41659)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:56:31 -07:00
Evan RusackasandClaude Opus 4.8 83965b4be8 chore(ci): correct codeql-action version pin comment to v4.36.2 (#41693)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:56:00 -07:00
Evan RusackasandClaude Opus 4.8 f9bcf189c9 chore(ci): suppress zizmor adhoc-packages on GHA validator install (#41694)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:55:49 -07:00
Evan RusackasandClaude Opus 4.8 1c74185a71 chore(ci): correct codeql-action version comment to match pinned SHA (#41695)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:55:40 -07:00
Mehmet Salih Yavuz 8bf3933972 fix(dashboard): show a not-found state for a deleted dashboard (#41686) 2026-07-02 22:15:25 +03:00
yousophandClaude Sonnet 4.6 19e94855a1 fix(explore): prevent Results FilterInput from stealing focus during remount (#41100)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 11:33:17 -07:00
Brian Maina 139df20cde fix(i18n): update German security menu translations (#41587) 2026-07-03 01:13:14 +07:00
Imad Helal 4c193d4dbc feat(i18n): wrap description strings in translation function (#41626) 2026-07-03 00:44:25 +07:00
aa40934e7f fix(i18n): compile fuzzy translations into the backend .mo files (#41648)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:43:13 -07:00
jackandEvan Rusackas 6c2c814b5c fix(dashboard): not filterable column now not emitting cross-filters in table charts (#30827)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-02 10:42:53 -07:00
Evan Rusackas 9769380d6d feat(i18n): backfill Polish (pl) translations (AI-generated, needs review) (#41660) 2026-07-03 00:40:28 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> be29d877d2 chore(deps-dev): bump @storybook/addon-docs from 10.4.5 to 10.4.6 in /superset-frontend (#41375)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 10:37:26 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e3b2992d6e chore(deps): update lodash-es requirement from ^4.17.21 to ^4.18.1 in /superset-frontend/packages/superset-ui-core (#41565)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 10:37:05 -07:00
Joe Li c1bd45f561 fix(ci): allow showtime to check out fork PR code under checkout v7 (#41643) 2026-07-03 00:31:43 +07:00
Đỗ Trọng Hải 7214e9f9f6 build(dev-deps): upgrade Storybook to v10 in docs subproject (#41679)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-02 10:31:01 -07:00
Đỗ Trọng Hải d7e2f18d00 fix(dockerfile): allow GH auth-less fetch of uv when building Superset image locally (#41682) 2026-07-03 00:30:03 +07:00
Luis Carbonell 6309d08d59 feat(helm): standardize to Kubernetes recommended labels (app.kubernetes.io/*) (#39350) 2026-07-03 00:26:35 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> afebdd58d1 chore(deps): bump docusaurus-theme-openapi-docs from 5.0.2 to 5.1.0 in /docs (#41669)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 10:25:41 -07:00
Mike BridgeandMike Bridge be46d65e3b fix(dao): SQL-faithful NULL and non-string handling in LIKE-family operators (#41653)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-07-02 10:04:35 -07:00
2992d7b4c8 feat(database): add databricks oauth support (#41421)
Co-authored-by: fabian_zse <fabian@zalando.de>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 09:26:08 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 80344852b7 chore(deps): bump docusaurus-plugin-openapi-docs from 5.0.2 to 5.1.0 in /docs (#41671)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 22:32:18 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Đỗ Trọng Hải
8210904e95 chore(deps): bump nanoid from 5.1.15 to 5.1.16 in /superset-frontend (#41673)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-07-02 22:31:16 +07:00
innovark 0a25faabbd fix(dashboard): align chart menu button hover state (#41683) 2026-07-02 21:47:05 +07:00
Mehmet Salih YavuzandEnzo Martellucci a48ca9ce72 fix(sqllab): reject blank saved query and dataset names (#41624)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-07-02 14:03:14 +03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8343a4fff8 chore(deps-dev): bump @types/node from 26.0.0 to 26.0.1 in /superset-websocket (#41666)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 16:33:36 +07:00
Mehmet Salih Yavuz a919dda2ac fix(sqllab): filter results table when typing in the search box (#41625) 2026-07-02 12:09:10 +03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 246bbeb408 chore(deps): bump google-auth-library from 10.7.0 to 10.9.0 in /superset-frontend (#41672)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 14:54:20 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b2af954e7b chore(deps-dev): bump @types/node from 26.0.0 to 26.0.1 in /superset-frontend (#41675)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 14:54:03 +07:00
Amin Ghadersohi c3f5e997a1 feat(mcp): chart type plugin registry for extensible generate_chart (#39922) 2026-07-02 00:31:19 -04:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe Li
d507be2555 chore(deps): bump geostyler-openlayers-parser from 5.7.0 to 5.7.1 in /superset-frontend (#41615)
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>
2026-07-02 10:04:12 +07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Đỗ Trọng HảiJoe Li
e3bd6e5c70 chore(deps-dev): bump @playwright/test from 1.61.0 to 1.61.1 in /superset-frontend (#41616)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-02 10:02:20 +07:00
748060d35e feat(i18n): backfill Thai (th) translations (AI-generated, needs review) (#41641)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:43:11 -07:00
d57d69d3a6 feat(i18n): backfill Ukrainian (uk) translations (AI-generated, needs review) (#41645)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:42:58 -07:00
706ff94f0b feat(i18n): backfill Slovak (sk) translations (AI-generated, needs review) (#41640)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:42:47 -07:00
661d362580 feat(i18n): backfill new safe-link strings in de/lv/fi (AI-generated, needs review) (#41646)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:42:38 -07:00
6bdcb7a83a feat(i18n): backfill Czech (cs) translations (AI-generated, needs review) (#41647)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:41:34 -07:00
Alejandro Solares d1f7dd9c67 fix(deps): override fast-uri to 3.1.3 to fix CVE-2026-13676 (#41631)
Signed-off-by: Alejandro Solares <219859296+ASolarers-Rodriguez@users.noreply.github.com>
2026-07-01 17:28:45 -07:00
c718f717cb feat(i18n): backfill Spanish (es) translations (AI-generated, needs review) (#41609)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:25:13 -07:00
innovarkandSBIN2010 b3197c9b5e fix(table-chart): fix "Search by" control visibility and improve table controls layout (#36073)
Co-authored-by: SBIN2010 <Sbin2010@mail.ru>
2026-07-01 17:22:36 -07:00
af0a55a4f3 feat(dashboards): soft-delete and restore (#40128)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-01 17:20:44 -07:00
8be255de40 chore(i18n): harden backfill_po — full language-name map + resilient batch translation (#41644)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:28:19 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f2e322c3c0 chore(deps-dev): bump sigstore from 4.1.0 to 4.1.1 in /superset-frontend (#41638)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 16:25:50 -07:00
Jean MassucattoandJoe Li e58ce1cf39 fix(dashboard): pre-filter time grain for display controls (#40000)
Co-authored-by: Joe Li <joe@preset.io>
2026-07-01 16:10:52 -07:00
393adc4535 refactor(db): composite PK on M2M association tables (#39859)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-01 13:07:15 -07:00
Amin Ghadersohi e0a3b1c10c fix(mcp): document select_columns valid fields and URL scheme for preview tools (#41595) 2026-07-01 15:58:12 -04:00
Evan RusackasandClaude Code 6c57919647 chore(codeowners): update maintainer assignments (#41634)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-01 12:25:13 -07:00
bed1034c2f refactor(frontend): centralize subdirectory URL prefixing behind nav helpers (#39925)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan <evan@preset.io>
2026-07-01 11:20:13 -07:00
Mehmet Salih Yavuz b7d5de8e52 fix(sqllab): truncate long tab names in the overflow ("...") dropdown (#41585) 2026-07-01 20:52:27 +03:00
Đỗ Trọng Hải 7d7c3ce723 feat(ci): install helm-docs directly instead of using whole brew setup (#41629) 2026-07-02 00:35:43 +07:00
692f81d945 feat(i18n): backfill Finnish (fi) translations (AI-generated, needs review) (#41613)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:27:28 -07:00
eeacd9b6dd feat(i18n): backfill Latvian (lv) translations (AI-generated, needs review) (#41612)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:27:07 -07:00
b3c709b3d5 feat(i18n): backfill German (de) translations (AI-generated, needs review) (#41608)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:24:20 -07:00
Nitish Agarwal 792d677634 fix(reports): respect CSV_EXPORT sep and decimal config in email reports (#38616) 2026-07-01 10:23:48 -07:00
2d2a72b721 fix(sqllab): show non-ASCII text in array/JSON columns instead of \uXXXX escapes (#41533)
Co-authored-by: Vladislav Korenkov <73882772+Quatters@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:22:11 -07:00
Evan RusackasandClaude Code 55b2da75f6 fix(echarts): allow forcing categorical x-axis for temporal columns (#41221)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-01 10:21:59 -07:00
Amin Ghadersohi 7f4cac63c9 fix(mcp): escape LIKE wildcards in find_users to prevent user enumeration (#41593) 2026-07-01 13:01:46 -04:00
Amin Ghadersohi da4cae1657 chore(deps): bump fastmcp from >=3.2.4 to >=3.4.2 (#41592) 2026-07-01 12:42:25 -04:00
Mehmet Salih Yavuz 438d4d569f test(sqllab): repair broken TablePreview and SavedQueryList jest tests (#41628) 2026-07-01 19:34:48 +03:00
6d2b94ceb8 fix(currency): derive default symbol position from locale when unset (#40931)
Co-authored-by: kleostouraiti <212892934+kleostouraiti@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:12:18 -07:00
ShaitanandClaude Opus 4.8 2da2db6c7c feat(sql): schema-qualified table denylist + information_schema/lo_* defaults (#41120)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:57:45 +01:00
3651020014 fix(sql): cap parser input length via SQL_MAX_PARSE_LENGTH config (#40499)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: sha174n <pedro.sousa@preset.io>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-01 16:32:12 +01:00
b9e3f0aa1e feat(soft-delete): gate soft delete behind a temporary SOFT_DELETE release toggle (#41166)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 08:09:52 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 16e1f41cef chore(deps): bump swagger-ui-react from 5.32.7 to 5.32.8 in /docs (#41614)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 21:27:37 +07:00
Raphael Prudencioandbito-code-review[bot] <188872107+bito-code-review[bot]@users.noreply.github.com> 7cc7e9f6e3 fix(async-query): prevent JWT InvalidSubjectError for guest users (#37862)
Co-authored-by: bito-code-review[bot] <188872107+bito-code-review[bot]@users.noreply.github.com>
2026-06-30 23:42:41 -07:00
ImamatdinandImamatdin 3e88b487b3 fix(point-cluster-map): guard invalid point radii (#40393)
Co-authored-by: Imamatdin <201577118+Imamatdin@users.noreply.github.com>
2026-06-30 22:48:43 -07:00
ce9b9b0513 feat(i18n): add Japanese (ja) translations (AI-generated, needs review) (#41466)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: aikawa-ohno <aikawa-ohno@users.noreply.github.com>
2026-06-30 17:33:27 -07:00
Evan RusackasandClaude Opus 4.8 35194fe4d5 fix(i18n): key translation-regression check on per-msgid transitions (#41596)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:54:39 -07:00
Evan RusackasandClaude Opus 4.8 2a1f632daa fix(dashboard): surface size, limit, and config key in oversized dashboard error (#41532)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:10:01 -07:00
Amin Ghadersohi fd9c84be43 feat(mcp): add get_dashboard_datasets tool (#40961) 2026-06-30 18:27:09 -04:00
Amin GhadersohiandClaude Sonnet 4.6 2bd9ab4c59 feat(mcp): add remove_chart_from_dashboard tool (#40958)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 18:16:08 -04:00
Amin GhadersohiandClaude Sonnet 4.6 bf88c62814 feat(mcp): add manage_native_filters tool (#40960)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 17:07:49 -04:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Joe Li
1e130feb80 chore(deps): bump marshmallow-union from 0.1.15 to 0.1.15.post1 (#41539)
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: Joe Li <joe@preset.io>
2026-06-30 13:54:45 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fd86eec889 chore(deps): bump echarts from 5.6.0 to 6.1.0 in /superset-frontend (#40264)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 13:54:11 -07:00
Evan RusackasandClaude Opus 4.7 a8f43890b1 chore(superset-ui-core): forward-compat fixes for TypeScript 6.0 (Phase B) (#39535)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 13:52:14 -07:00
Evan RusackasandClaude Code 4bf203ee70 chore(config): default SMTP_SSL_SERVER_AUTH to True (#40647)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-30 13:50:38 -07:00
709 changed files with 121117 additions and 36386 deletions
+7 -7
View File
@@ -1,7 +1,5 @@
# Notify all committers of DB migration changes, per SIP-59
# https://github.com/apache/superset/issues/13351
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe
# Notify some committers of changes in the components
@@ -12,28 +10,30 @@
# Notify Helm Chart maintainers about changes in it
/helm/superset/ @craig-rueda @dpgaspar @villebro @nytai @michael-s-molina @mistercrunch @rusackas @Antonio-RiveroMartnez
/helm/superset/ @dpgaspar @villebro @nytai @michael-s-molina @mistercrunch @rusackas @Antonio-RiveroMartnez @hainenber
# Notify E2E test maintainers of changes
/superset-frontend/cypress-base/ @sadpandajoe @geido @eschutho @rusackas @betodealmeida @mistercrunch
/superset-frontend/playwright/ @sadpandajoe @geido @eschutho @rusackas @mistercrunch
/superset-frontend/cypress-base/ @sadpandajoe @geido @eschutho @rusackas @mistercrunch
# Notify PMC members of changes to GitHub Actions
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @sadpandajoe @hainenber
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @sha174n @dpgaspar @sadpandajoe @hainenber
# Notify PMC members of changes to CI-executed scripts (supply-chain risk:
# scripts/ files run directly in CI workflows and can execute arbitrary code)
/scripts/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @sadpandajoe @hainenber
/scripts/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @dpgaspar @sha174n @sadpandajoe @hainenber
# Notify PMC members of changes to required GitHub Actions
/.asf.yaml @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @Antonio-RiveroMartnez
/.asf.yaml @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @dpgaspar @sha174n @Antonio-RiveroMartnez
# Maps are a finicky contribution process we care about
**/*.geojson @villebro @rusackas
**/*.ipynb @villebro @rusackas
/superset-frontend/plugins/legacy-plugin-chart-country-map/ @villebro @rusackas
# Notify translation maintainers of changes to translations
-11
View File
@@ -10,11 +10,7 @@ updates:
- package-ecosystem: "npm"
ignore:
- dependency-name: "react-error-boundary"
- dependency-name: "@rjsf/*"
# remark-gfm v4+ requires react-markdown v9+, which needs React 18
- dependency-name: "remark-gfm"
- dependency-name: "react-markdown"
# TODO: remove below entries until React >= 19.0.0
- dependency-name: "react-icons"
# JSDOM v30 doesn't play well with Jest v30
@@ -78,8 +74,6 @@ updates:
- package-ecosystem: "npm"
directory: "/docs/"
ignore:
- dependency-name: "react-error-boundary"
schedule:
interval: "daily"
open-pull-requests-limit: 10
@@ -376,11 +370,6 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-core/"
ignore:
# not until React >= 18.0.0
- dependency-name: "react-markdown"
- dependency-name: "remark-gfm"
- dependency-name: "react-error-boundary"
schedule:
interval: "daily"
labels:
+2 -2
View File
@@ -64,7 +64,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -75,6 +75,6 @@ jobs:
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:${{matrix.language}}"
@@ -37,8 +37,9 @@ jobs:
node-version: "20"
- name: Install Dependencies
# Versions are pinned to avoid ad-hoc, unpinned package installs
# (zizmor adhoc-packages). Bump deliberately when upgrading.
# Versions are pinned to avoid ad-hoc, unpinned package installs.
# Bump deliberately when upgrading.
# zizmor: ignore[adhoc-packages] - @action-validator is a global CLI tool installed to validate the repo's workflows; a global CLI install has no application manifest/lockfile context, and the versions are pinned above
run: npm install -g @action-validator/core@0.6.0 @action-validator/cli@0.6.0
- name: Run Script
+8 -9
View File
@@ -32,19 +32,18 @@ jobs:
with:
persist-credentials: false
submodules: recursive
- name: Setup Python
uses: ./.github/actions/setup-backend/
with:
python-version: ${{ matrix.python-version }}
- name: Enable brew and helm-docs
# Add brew to the path - see https://github.com/actions/runner-images/issues/6283
run: |
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
echo "HOMEBREW_PREFIX=$HOMEBREW_PREFIX" >>"${GITHUB_ENV}"
echo "HOMEBREW_CELLAR=$HOMEBREW_CELLAR" >>"${GITHUB_ENV}"
echo "HOMEBREW_REPOSITORY=$HOMEBREW_REPOSITORY" >>"${GITHUB_ENV}"
brew install norwoodj/tap/helm-docs
- name: Setup Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
- name: Install helm-docs
run: go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
+6 -1
View File
@@ -2,7 +2,8 @@ name: 🎪 Superset Showtime
# Ultra-simple: just sync on any PR state change
on:
# zizmor: ignore[dangerous-triggers] - required to react to PR label changes; this workflow does not check out or execute PR-provided code
# zizmor: ignore[dangerous-triggers] - required to react to PR label changes; PR code is
# only checked out and built after the maintainer-authorization gate (write/admin actors)
pull_request_target:
types: [labeled, unlabeled, synchronize, closed]
@@ -156,6 +157,10 @@ jobs:
with:
ref: ${{ steps.check.outputs.target_sha }}
persist-credentials: false
# Building fork PR code is Showtime's purpose: deploys are gated on the
# maintainer-authorization step above (write/admin actors only), so this
# checkout is an explicit, authorized opt-in rather than an automatic one.
allow-unsafe-pr-checkout: true
- name: Setup Docker Environment (only if build needed)
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
Regular → Executable
View File
+2 -2
View File
@@ -120,7 +120,7 @@ RUN useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash
# Some bash scripts needed throughout the layers
COPY --chmod=755 docker/*.sh /app/docker/
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN pip install --no-cache-dir --upgrade uv
# Using uv as it's faster/simpler than pip
RUN uv venv /app/.venv
@@ -141,7 +141,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
COPY superset/translations/ /app/translations_mo/
RUN if [ "${BUILD_TRANSLATIONS}" = "true" ]; then \
pybabel compile -d /app/translations_mo | true; \
pybabel compile --use-fuzzy -d /app/translations_mo || true; \
fi; \
rm -f /app/translations_mo/*/*/*.[po,json]
+143
View File
@@ -24,6 +24,47 @@ assists people when migrating to a new version.
## Next
- [39925](https://github.com/apache/superset/pull/39925): URL prefixing for `SUPERSET_APP_ROOT` subdirectory deployments is now handled automatically by helpers in `src/utils/navigationUtils` (`openInNewTab`, `redirect`, `getShareableUrl`, `<AppLink>`). Direct imports of `ensureAppRoot` / `makeUrl` from `src/utils/pathUtils` are forbidden outside `navigationUtils.ts` (enforced by a static-invariant test); contributors writing new code should use the focused helpers instead. No runtime behaviour change for existing callers — all 19 prior call sites have been migrated and four pre-existing double-prefix and missing-prefix bugs are fixed as part of the migration.
- [39925](https://github.com/apache/superset/pull/39925): `SupersetClient.getUrl()` now strips a single leading application-root segment from the supplied `endpoint` before building the request URL, so a caller that accidentally pre-prefixes its endpoint (for example by wrapping it with `ensureAppRoot` before passing it to the client) no longer produces a doubled `/superset/superset/...` URL under subdirectory deployment. The strip is **single-pass** — a genuine `/superset/superset/<slug>` route is preserved, not collapsed — and **silent** (no console warning); the static-invariant test remains the primary signal for pre-prefixing at the call site, and this runtime strip is a safety net beneath it. Code that intentionally targeted a literal `/<app_root>/<app_root>/...` endpoint through `getUrl` (a configuration that has no legitimate use under the prefixing model) would have its first redundant segment removed.
- **Breaking — `Superset` view class route prefix removed.** The `Superset` view in `superset/views/core.py` now declares `route_base = ""`, overriding Flask-AppBuilder's auto-derived `/superset` prefix. Routes that previously lived at `/superset/welcome/`, `/superset/dashboard/<id>/`, `/superset/dashboard/p/<key>/`, `/superset/explore/`, etc. now respond at `/welcome/`, `/dashboard/<id>/`, `/dashboard/p/<key>/`, `/explore/`, etc. Under subdirectory deployment (`SUPERSET_APP_ROOT=/superset`) the URLs are unchanged from end-user perspective — `AppRootMiddleware` re-applies the prefix via `SCRIPT_NAME`. Under root deployments, any external integration or bookmark that hard-codes `/superset/<endpoint>/` paths must be updated to drop the prefix. This fixes the doubled `/superset/superset/...` URLs that `url_for` emitted for these endpoints under subdirectory deployment and the related 404s on the routes themselves.
- **Breaking — Three sibling view classes route prefix removed.** Following the same rationale as the `Superset` class above, `ExplorePermalinkView` (`superset/views/explore.py`), `TagModelView`, and `TaggedObjectsModelView` (`superset/views/tags.py`, `superset/views/all_entities.py`) now mount at the application root rather than a hard-coded `/superset/...`. The user-visible URLs `/superset/explore/p/<key>/`, `/superset/tags/`, and `/superset/all_entities/` are unchanged under subdirectory deployment; under root deployments these views now serve `/explore/p/<key>/`, `/tags/`, and `/all_entities/`, so any external integration or bookmark must drop the `/superset/` prefix. `Dashboard.url` and `Dashboard.get_url` likewise return `/dashboard/<id>/` instead of the prior `/superset/dashboard/<id>/` literal so downstream consumers (DashboardList row hrefs, MCP service `dashboard_url`) emit a single, deployment-correct prefix.
- **Legacy `/superset/*` path support.** A new outermost WSGI middleware `LegacyPrefixRedirectMiddleware` (`superset/middleware/legacy_prefix_redirect.py`) 308-redirects every enumerated legacy `/superset/<canonical>` path to its post-`route_base=""` canonical location (e.g. `/superset/welcome/``/welcome/` under root; → `/superset/welcome/` under `SUPERSET_APP_ROOT=/superset`, because the canonical resolves through `AppRootMiddleware`). Bookmarks, email links, and external integrations survive the route-base collapse for one release cycle. POST against a GET-only canonical returns 410 Gone instead of 308 (308 would 405 on retry). The shim is removed at EOL `5.0.0`, matching the `@deprecated(eol_version="5.0.0")` gate on `Superset.explore` and `Superset.explore_json`.
- **PWA web app manifest served dynamically.** The PWA manifest is now served at `/pwa-manifest.json` (under `APPLICATION_ROOT`) by a new `PwaManifestView` (`superset/views/pwa_manifest.py`) instead of the static file at `/static/assets/pwa-manifest.json`. The legacy static source at `superset-frontend/src/pwa-manifest.json` has been removed (along with its `webpack.config.js` `CopyPlugin` rule). The new endpoint resolves `APPLICATION_ROOT` and `STATIC_ASSETS_PREFIX` at request time so PWA install works under subdirectory deployments and split static-prefix / app-root deployments (where `STATIC_ASSETS_PREFIX` points to a CDN host while the Superset backend stays under `APPLICATION_ROOT`). The `<link rel="manifest">` href in `superset/templates/superset/spa.html` was updated correspondingly (using a new `application_root_rstrip` template global). Operators with a forked `spa.html` should switch any manifest `<link>` to `{{ application_root_rstrip }}/pwa-manifest.json`.
- **Hard re-bookmark break — `/superset/sql/<database_id>/`.** SQL Lab moved to its own blueprint at `/sqllab/`. The legacy `/superset/sql/<id>/` shape changed to a query-string form (`/sqllab/?dbid=<id>`); no 1:1 path mapping exists, so `LegacyPrefixRedirectMiddleware` does **not** redirect this route — it passes through and surfaces a 404. Users with bookmarks to `/superset/sql/<id>/` must update them to `/sqllab/?dbid=<id>`.
- **`SqlaTable.sql_url` query-string format.** `SqlaTable.sql_url` now URL-encodes `table_name` and joins it as a query parameter rather than concatenating a second `?`. Previously, with `Database.sql_url` returning `/sqllab/?dbid=<id>`, the concatenation produced `/sqllab/?dbid=<id>?table_name=<raw>` — a malformed second `?` that broke the query parser. External code that parsed the legacy `<base>?table_name=<raw>` shape now sees properly percent-encoded values (e.g. `/``%2F`, ` ``+` or `%20`); decode with `urllib.parse.parse_qsl`.
- **New config flag `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE` (default `False`).** Share/permalink URLs now substitute `window.location.origin` for the backend-supplied origin so a proxied or subdirectory-deployed Superset never hands the user an unreachable internal hostname. Operators whose reverse proxy correctly forwards `X-Forwarded-Host` *and* who want permalinks to carry the backend's literal origin can opt out by setting `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True` in `superset_config.py`. Default `False` (rewrite is on); flipping the default would regress the dominant proxied/subdir deployment to an unreachable host.
- [41651](https://github.com/apache/superset/pull/41651): **New do-not-translate standard for translation catalogs.** Strings that must stay identical to the source — icon names (e.g. `bolt`), enum/option values (`step-after`), SQL keywords, API field names (`error_message`), code constants, and example placeholders — are now marked with a `#. do-not-translate` extracted comment. The list lives in the `superset/translations/do-not-translate.txt` registry; `scripts/translations/apply_do_not_translate.py` stamps the marker onto `messages.pot` during `babel_update.sh`, and `pybabel update` propagates it to every `.po`, so the status is consistent across all languages. The AI backfill (`backfill_po.py`) and translators leave these entries untranslated (source fallback). The legacy per-catalog convention (a `# Не переводить` translator comment in the `ru` catalog) is still honored for back-compat but is superseded by this standard; contributors adding new machine-read strings should add the msgid to the registry rather than annotating individual catalogs.
### SQL Lab denies large-object and information_schema access by default
`DISALLOWED_SQL_FUNCTIONS` and `DISALLOWED_SQL_TABLES` now ship with additional default entries, so SQL Lab and chart-data queries that reference them are rejected where they were previously allowed:
- PostgreSQL large-object routines (`lo_from_bytea`, `lo_export`, `lo_import`, `lo_put`, `lo_create`, `lo_creat`, `lowrite`, `lo_get`, `loread`, `lo_unlink`), which read and write bytes on the database server's filesystem.
- The SQL-standard `information_schema` views (`tables`, `columns`, `routines`, `views`, the privilege/grant views, etc.), which expose table, column, privilege, and view-definition metadata across the whole database.
Deployments that legitimately query these (for example tooling that introspects `information_schema`) can restore the previous behavior by overriding `DISALLOWED_SQL_FUNCTIONS` / `DISALLOWED_SQL_TABLES` in `superset_config.py` to drop the entries they need.
Because the denylist now resolves the effective schema through the query-aware path, PostgreSQL queries that change the `search_path` (e.g. `SET search_path = ...`) are rejected on the SQL Lab execution and cost-estimate paths whenever any `DISALLOWED_SQL_TABLES` entry is configured (the default for PostgreSQL), matching the behavior previously applied only when `RLS_IN_SQLLAB` was enabled.
### SQL parser input length cap (SQL_MAX_PARSE_LENGTH)
The SQL parser now rejects scripts whose UTF-8 byte length exceeds the new
`SQL_MAX_PARSE_LENGTH` config option (default `1_000_000` bytes) before they are
handed to sqlglot, which bounds parser memory and CPU usage. A single query
larger than the cap (for example a very large `IN (...)` list or a big
virtual-dataset SQL) raises a parse error in SQL Lab and dashboard-generated
queries. Deployments that legitimately run queries above this size should raise
the value, and `SQL_MAX_PARSE_LENGTH = None` disables the check entirely.
### Guest-token RLS rules reject unknown fields
The `rls` rules passed to `POST /api/v1/security/guest_token/` are now validated strictly: a rule may only contain `dataset` and `clause`. Previously unknown fields were silently dropped, so a mistyped or legacy scope key (most commonly `datasource` instead of `dataset`) produced a rule with no `dataset`, which is treated as a *global* rule applied to every dataset the embedded resource can reach. Such a request now returns HTTP 400 identifying the offending field instead of issuing a token with an unintended global rule. Integrators that were sending extra fields in RLS rules must remove them; valid dataset-scoped (`{"dataset": 41, "clause": "..."}`) and global (`{"clause": "..."}`) rules are unaffected.
@@ -40,10 +81,27 @@ When the MCP service has JWT auth enabled (`MCP_AUTH_ENABLED = True`), an audien
The git SHA and build number surfaced in the "About" section, the bootstrap payload, and the public `/version` endpoint are now only included for admin users by default; the release version string is still shown to everyone. To expose the build details to all users (the previous behavior), set the `SUPERSET_EXPOSE_BUILD_DETAILS` environment variable (or `EXPOSE_BUILD_DETAILS_TO_USERS = True` in `superset_config.py`).
### Helm chart adopts Kubernetes recommended labels (breaking upgrade)
The Helm chart now labels and selects workloads using the [Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) (`app.kubernetes.io/*`) instead of the legacy `app`/`release` labels. Because a Deployment's `spec.selector.matchLabels` is immutable, `helm upgrade` against an existing release will fail with a `field is immutable` error.
To upgrade, delete the affected workloads (which selector labels changed) before upgrading, then run the upgrade so they are recreated with the new labels:
```bash
kubectl delete deployment,statefulset -l release=<release-name> -n <namespace>
helm upgrade <release-name> superset/superset
```
Alternatively, perform a fresh install. This is a one-time migration; subsequent upgrades are unaffected.
### Pivot table First/Last aggregations follow data order
The pivot table chart's `First` and `Last` aggregations now return the first and last value in data (query result) order, instead of effectively returning the minimum and maximum. Existing pivot tables that use these aggregations for totals/subtotals may show different values after upgrading. For deterministic results, ensure the underlying query has a stable sort order.
### `FetchRetryOptions` callback parameters widened to allow `null`
The `error` and `response` parameters of the `retryDelay` and `retryOn` callbacks in `FetchRetryOptions` (exported from `@superset-ui/core`) are now typed `Error | null` and `Response | null` to match the actual call-site signature provided by `fetch-retry`. Because these parameter types are contravariant, consumers who typed their callbacks with the non-nullable `(attempt: number, error: Error, response: Response) => number` will get a TypeScript compile error. Widen your callback signatures to accept `Error | null` / `Response | null`.
### `thumbnail_url` removed from dashboard list API response
The `thumbnail_url` field has been removed from `GET /api/v1/dashboard/` list responses. External consumers relying on this field must now construct the thumbnail URL client-side using `id` and `changed_on_utc`:
@@ -77,6 +135,7 @@ Deployments that intentionally point webhooks at internal targets (chatops bridg
### Impala cancel_query blocks private/internal hosts by default
The Impala engine spec's `cancel_query` issues an HTTP request from the Superset backend to the host configured on the Impala database connection. That host is now validated before the request: if it resolves to a private/internal IP range, the cancel call is refused and a warning is logged. Operators whose Impala cluster runs on an internal network can opt out by setting `IMPALA_CANCEL_QUERY_ALLOW_INTERNAL_HOSTS = True` in `superset_config.py`. This mirrors the dataset-import and webhook opt-out flags.
### Map chart renderer and OpenStreetMap migration behavior
The MapLibre migration for deck.gl charts preserves saved non-Mapbox styles on
@@ -114,6 +173,11 @@ Operators can tune or disable the policy via config:
### Data uploads bounded by UPLOAD_MAX_FILE_SIZE_BYTES
Single data-file uploads (CSV, Excel, columnar) are now bounded by the `UPLOAD_MAX_FILE_SIZE_BYTES` config option, which defaults to `100 * 1024 * 1024` (100 MB). Files larger than this are rejected with a `413` before their contents are buffered into memory. Set `UPLOAD_MAX_FILE_SIZE_BYTES = None` to disable the check and restore unbounded uploads.
### Currency symbol position follows the locale when unset
When a chart's currency control leaves the **Prefix or suffix** field empty, the currency symbol position is now derived from the deployment locale's own convention via `Intl.NumberFormat` instead of always defaulting to a suffix. For example, under the default `en-US` locale `USD`, `GBP`, and `EUR` render as a prefix (`$ 1,000`), while eurozone locales such as `fr-FR` render `EUR` as a suffix (`1 000 €`). An explicit Prefix/Suffix selection is always honored and is unaffected.
Charts that relied on the previous always-suffix default for an unset position will render the symbol on the locale-appropriate side instead; set the position explicitly on the metric's currency control to pin it.
### Duration formatter precision
@@ -179,6 +243,18 @@ Runbook to adopt:
2. Set that value on the tunnel's `server_host_key` (via the database/SSH tunnel API or UI payload).
3. Optionally set `SSH_TUNNEL_STRICT_HOST_KEY_CHECKING = True` in `superset_config.py` to require host-key verification on all tunnels.
### SMTP server certificate validation enabled by default
`SMTP_SSL_SERVER_AUTH` now defaults to `True` (previously `False`). With this default, STARTTLS/SSL connections to the configured SMTP server validate the server's TLS certificate against the system trusted CA store. This makes outbound email (alerts and reports) verify the mail server's identity out of the box.
If your SMTP server presents a self-signed certificate, or a certificate that is not trusted by the system CA store, email delivery may now fail with a certificate verification error. To restore the previous behavior of skipping certificate validation, set the following in `superset_config.py`:
```python
SMTP_SSL_SERVER_AUTH = False
```
The recommended fix is to add the SMTP server's certificate (or its issuing CA) to the system trust store rather than disabling validation.
### Dataset import validates catalog against the target connection
Importing a dataset now validates the `catalog` field against the target database connection. When the connection has multi-catalog disabled (`allow_multi_catalog` off) and the dataset's catalog is not the connection's default catalog, the import fails instead of silently persisting the non-default catalog. This matches the validation already enforced on the dataset update path and prevents imported datasets from querying an unintended database.
@@ -228,6 +304,50 @@ Schedule the cutover in a quiet window. Runtime reads use only the single config
The migration is transactional (all-or-nothing) and idempotent — it can be safely re-run or resumed. Note that AES-GCM, unlike AES-CBC, does not support querying directly over encrypted columns; audit any code that filters on an encrypted column before switching. See the SIP at `docs/sip/authenticated-encryption-at-rest.md` for details.
### Soft delete and restore for charts
**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/chart/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
**Flag-toggle caveat:** the soft-delete visibility filter is evaluated per query while the flag is on. If charts are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live charts in all lists, lookups, and relationship loads (including dashboards that contained them). The `POST /<uuid>/restore` endpoint and the `chart_deleted_state` list filter remain functional regardless of the flag, deliberately, so rows soft-deleted during a flag-on window stay discoverable and restorable after a rollback of the flag.
With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the chart (the bulk-delete endpoint behaves the same way). The row is marked with a `deleted_at` timestamp and hidden from all list, detail, and lookup endpoints. Charts in this state are excluded from default queries and from relationship loads (e.g. `dashboard.slices`).
**Operational notes:** a report schedule whose target chart is soft-deleted now fails its runs with an explicit error ("The chart this report targets was deleted...") until the chart is restored or the report re-pointed — chart deletion is blocked while a report references the chart, but a validate/commit race or a flag toggle can still produce this state. Dashboards **preserve** their membership rows for soft-deleted charts: saving a dashboard does not sever a trashed member, and restoring the chart re-attaches it to its dashboards.
**New endpoint** — `POST /api/v1/chart/<uuid>/restore` clears `deleted_at` and returns the chart to active state. Requires `can_write on Chart` and ownership of the row (or admin). Soft-deleted charts can also be surfaced in the list endpoint via the new `chart_deleted_state` rison filter: `include` returns both live and soft-deleted rows, `only` returns just the soft-deleted ones. Any other value is ignored. For non-admin users, soft-deleted rows are limited to charts they own — the same audience that can restore them.
**Permissions migration:** existing role grants of `can_write on Chart` cover the new restore endpoint automatically; no role migration is required.
**Schema migration:** the migration adds a nullable `deleted_at` column and an index on it (`ix_slices_deleted_at`) to the `slices` table. The column add is instant; the index build runs inline (no `CONCURRENTLY`) and may briefly block writes on the `slices` table (INSERT/UPDATE/DELETE are queued while the index builds; reads are unaffected) on large Postgres deployments. MySQL InnoDB builds the index online (no blocking).
**Rollback note:** if the application code is rolled back after charts have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted charts silently become live, active charts with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-owners get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
- [39914](https://github.com/apache/superset/pull/39914) `ALERT_REPORT_SLACK_V2` now defaults to `True` and the legacy Slack v1 integration (`Slack` recipient type, `files.upload` API) is deprecated for removal in the next major. Slack blocked new apps from `files.upload` in May 2024 and fully retired the method for all apps on November 12, 2025; because the v1 path sends files through `files.upload`, v1 file-bearing sends now fail at the API level — only text-only `chat_postMessage` still works via the legacy path. Grant your Slack bot the `channels:read` and `groups:read` scopes so existing `Slack` recipients can be auto-upgraded to `SlackV2` on next send. Operators who explicitly override the flag to `False`, or whose Slack bot is missing those scopes, will see deprecation warnings while text-only sends continue through the legacy path.
### Soft delete and restore for dashboards
**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/dashboard/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
**Flag-toggle caveat:** the soft-delete visibility filter is evaluated per query while the flag is on. If dashboards are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live dashboards in all lists and lookups (including slug lookups — if a soft-deleted dashboard's slug was reused while the flag was on, both rows become visible with the same slug). The `POST /<uuid>/restore` endpoint and the `dashboard_deleted_state` list filter remain functional regardless of the flag, deliberately, so rows soft-deleted during a flag-on window stay discoverable and restorable after a rollback of the flag.
With the flag enabled: `DELETE /api/v1/dashboard/<id>` no longer hard-deletes the dashboard (the bulk-delete endpoint behaves the same way). The row is marked with a `deleted_at` timestamp and hidden from the dashboard API's list, detail, and lookup endpoints, which return 404 for soft-deleted dashboards. The embedded-dashboard iframe URL (`/embedded/<uuid>`) keeps rendering because it reads only `embedded.allowed_domains` and `embedded.dashboard_id` (the FK column) without dereferencing the parent dashboard; the frontend's subsequent dashboard-API fetch is what sees the 404 and surfaces "dashboard not found" to the user.
**New endpoint** — `POST /api/v1/dashboard/<uuid>/restore` clears `deleted_at` and returns the dashboard to active state. Requires `can_write on Dashboard` and ownership of the row (or admin). Soft-deleted dashboards can also be surfaced in the list endpoint via the new `dashboard_deleted_state` rison filter: `include` returns both live and soft-deleted rows, `only` returns just the soft-deleted ones. Any other value is ignored. For non-admin users, soft-deleted rows are limited to dashboards they own — the same audience that can restore them.
**Permissions migration:** existing role grants of `can_write on Dashboard` cover the new restore endpoint automatically; no role migration is required.
**Schema migration:** the migration adds a nullable `deleted_at` column and an index on it (`ix_dashboards_deleted_at`) to the `dashboards` table, and **replaces the full unique constraint on `slug`** with a partial unique index (`ix_dashboards_active_slug`) enforcing slug uniqueness only among active (non-soft-deleted) rows. The column add is instant. On Postgres the constraint swap briefly blocks reads and writes during `ALTER TABLE ... DROP CONSTRAINT` (acquires `ACCESS EXCLUSIVE`), then blocks writes only during `CREATE UNIQUE INDEX` (acquires `ShareLock`); reads pass through during the index build. Both windows are sub-second on a typical `dashboards` table. MySQL InnoDB builds the functional index online (no blocking).
**Rollback note:** the downgrade restores the original full unique constraint on `slug`. If the partial-index window allowed slug reuse (a soft-deleted row and an active row holding the same slug), `ALTER TABLE ... ADD CONSTRAINT idx_unique_slug UNIQUE (slug)` will abort with a unique-constraint violation. Before downgrading, hard-delete the soft-deleted duplicates (or rename one side) so each slug appears at most once across all rows. Rolling back the application code while leaving the new migration in place is also possible but exposes soft-deleted rows to the older code path; pair the rollback with a data decision (restore, hard-delete, or migrate-down).
The partial-index replacement is dialect-dependent: PostgreSQL uses a native `WHERE deleted_at IS NULL` partial index; MySQL 8.0.13+ uses a functional index over `(CASE WHEN deleted_at IS NULL THEN slug END)` (8.0.13 is the first release with functional key parts). **MySQL <8.0.13, MariaDB, and SQLite keep the original full unique constraint** (functional indexes / column-level UNIQUE recreation aren't supported cleanly — MariaDB is excluded even at 10.x because its `CASE`-expression index semantics differ), so on those backends a soft-deleted dashboard continues to reserve its slug for the lifetime of the row.
**Slug semantics:** on PostgreSQL and MySQL 8.0.13+, the slug of a soft-deleted dashboard is **free for reuse**. A new active dashboard can claim it immediately. Restoring a soft-deleted dashboard whose slug has since been claimed returns **422 with a clean error** (`DashboardSlugConflictError`) — rename one of the dashboards and retry; the restore is not silently rejected by a database-level constraint violation.
**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, role grants, owners, tags) — including role grants that were implicitly revoked by the deletion. Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and ownership of the deleted row (or admin) — non-owners get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
### Granular Export Controls
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
@@ -517,6 +637,29 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
}
```
### Composite primary keys on many-to-many association tables
Eight M:N association tables move from a synthetic `id INTEGER PRIMARY KEY` to a composite `PRIMARY KEY (fk1, fk2)` on their two foreign-key columns. The surrogate `id` is dropped, and the redundant `UNIQUE (fk1, fk2)` on the two tables that carried one is removed (now subsumed by the PK).
| Table | Composite PK |
|---|---|
| `dashboard_roles` | `(dashboard_id, role_id)` |
| `dashboard_slices` | `(dashboard_id, slice_id)` |
| `dashboard_user` | `(user_id, dashboard_id)` |
| `report_schedule_user` | `(user_id, report_schedule_id)` |
| `rls_filter_roles` | `(role_id, rls_filter_id)` |
| `rls_filter_tables` | `(table_id, rls_filter_id)` |
| `slice_user` | `(user_id, slice_id)` |
| `sqlatable_user` | `(user_id, table_id)` |
**Before upgrading:**
- The migration **deletes** two classes of pre-existing rows the composite PK cannot accommodate: duplicate `(fk1, fk2)` pairs (it keeps the lowest `id` and removes the rest) and rows with `NULL` in either FK column. Both are meaningless for `secondary=` association tables, but export the affected rows first if you need an audit record.
- External tooling (BI tools, backup scripts) that references the surrogate `id` on these tables will break; no application code references it.
- Downgrade restores the `id` column (and the original `UNIQUE` on the two tables that had it) but leaves the FK columns `NOT NULL` (intentional — a `NULL` FK in a junction row is meaningless).
For large `dashboard_slices` / `report_schedule_user` tables, see the operator runbook in [#39859](https://github.com/apache/superset/pull/39859) — pre-flight inventory queries, per-dialect lock-window sizing, and the duplicate / NULL-FK roll-up — to plan the maintenance window.
## 6.0.0
- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading.
- [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact.
+1 -19
View File
@@ -1,25 +1,7 @@
{/*
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.
*/}
---
title: AWS IAM Authentication
sidebar_label: AWS IAM Authentication
version: 1
sidebar_position: 15
---
@@ -549,6 +549,24 @@ CELERY_BEAT_SCHEDULE = {
Adjust `retention_period_days` to control how long query rows are kept. Companion opt-in tasks (`prune_logs`, `prune_tasks`) exist for pruning the logs and tasks tables; see the commented-out examples in `superset/config.py`. Without enabling these tasks, the metadata database will grow unbounded over time.
## Dashboard Layout Size Limit
Each dashboard stores its layout (the position, size, and nesting of every chart, row, and tab) as a JSON blob in the metadata database. Superset caps the length of this serialized blob with `SUPERSET_DASHBOARD_POSITION_DATA_LIMIT`, which defaults to `65535`:
```python
SUPERSET_DASHBOARD_POSITION_DATA_LIMIT = 65535
```
This is a Python-level cap (65535 is 2¹⁶ 1), independent of the database column capacity — the `position_json` column is a `MEDIUMTEXT`, which holds far more. When the serialized layout reaches this limit, the editor blocks the save and reports the current length, the limit, and this setting's name. A warning is shown once the layout passes 90% of the limit.
Large dashboards — for example, many charts spread across nested tabs — can exceed the default. Because the underlying column comfortably stores larger values, you can safely raise the limit:
```python
SUPERSET_DASHBOARD_POSITION_DATA_LIMIT = 131072 # double the default
```
Alternatively, split a very large dashboard into several smaller ones. Note that this check is enforced when saving layout edits in the UI; a dashboard imported from a ZIP with an oversized layout will load and render, but cannot be edited and re-saved until the limit is raised.
:::resources
- [Blog: Feature Flags in Apache Superset](https://preset.io/blog/feature-flags-in-apache-superset-and-preset/)
:::
+26 -2
View File
@@ -332,15 +332,39 @@ cd superset-frontend
npm run build-translation
# Backend
pybabel compile -d superset/translations
pybabel compile --use-fuzzy -d superset/translations
```
`--use-fuzzy` includes `#, fuzzy` entries in the compiled `.mo` files. Superset
serves fuzzy translations on purpose: the frontend build (`po2json --fuzzy`)
already includes them, `flask fab babel-compile` (used by the release images)
compiles with `-f`, and the production `Dockerfile` compiles with `--use-fuzzy`
as well. This keeps machine-generated (and other draft) translations visible in
the UI rather than falling back to English while they await review.
### Backfilling missing translations with AI
For languages with many untranslated strings, the repo includes a script that
uses Claude AI to generate draft translations for any missing entries. All
AI-generated strings are marked `#, fuzzy` and tagged with an attribution
comment so that human reviewers know they need to be checked before merging.
comment so that human reviewers know they need to be checked.
Note that `#, fuzzy` marks a translation as *needing review*, not as *withheld*:
both the frontend and backend builds serve fuzzy entries (see [Applying
translations](#applying-translations) above), so an AI-generated string is shown
in the UI as soon as it is built and deployed. Reviewers should verify each
entry and remove the `#, fuzzy` flag to promote it to a confirmed translation.
The script never touches entries that must stay literal — icon names, enum
values, SQL keywords, API field names, and example placeholders. These are
registered in `superset/translations/do-not-translate.txt`;
`scripts/translations/apply_do_not_translate.py` stamps them in `messages.pot`
with a `#. do-not-translate` extracted comment (run automatically
from `babel_update.sh`), which `pybabel update` then propagates to every
catalog. To mark a new string do-not-translate, add its msgid to the registry.
The backfill also honors that marker and any legacy do-not-translate translator
comment (e.g. the `ru` catalog's `# Не переводить`), leaving such entries
untranslated so they fall back to the source token.
#### Prerequisites
+1 -1
View File
@@ -749,7 +749,7 @@ const config: Config = {
showReadingTime: true,
// Please change this to your repo.
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/blog/',
'https://github.com/apache/superset/tree/master/docs',
},
theme: {
customCss: require.resolve('./src/styles/custom.css'),
+14 -23
View File
@@ -58,26 +58,15 @@
"@fontsource/inter": "^5.2.8",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@storybook/addon-docs": "^8.6.18",
"@storybook/blocks": "^8.6.15",
"@storybook/channels": "^8.6.18",
"@storybook/client-logger": "^8.6.18",
"@storybook/components": "^8.6.18",
"@storybook/core": "^8.6.18",
"@storybook/core-events": "^8.6.18",
"@storybook/csf": "^0.1.13",
"@storybook/docs-tools": "^8.6.18",
"@storybook/preview-api": "^8.6.18",
"@storybook/theming": "^8.6.15",
"@storybook/addon-docs": "^10.4.5",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.43",
"antd": "^6.4.5",
"baseline-browser-mapping": "^2.10.38",
"antd": "^6.5.0",
"baseline-browser-mapping": "^2.10.40",
"caniuse-lite": "^1.0.30001799",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
"js-yaml": "^5.1.0",
"js-yaml-loader": "^1.2.2",
"docusaurus-plugin-openapi-docs": "^5.1.0",
"docusaurus-theme-openapi-docs": "^5.1.0",
"js-yaml": "^5.2.0",
"json-bigint": "^1.0.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
@@ -88,8 +77,8 @@
"react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.2.0",
"storybook": "^8.6.18",
"swagger-ui-react": "^5.32.7",
"storybook": "^10.4.5",
"swagger-ui-react": "^5.32.8",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",
"unist-util-visit": "^5.1.0"
@@ -107,10 +96,10 @@
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.7.0",
"prettier": "^3.8.4",
"prettier": "^3.9.1",
"typescript": "~6.0.3",
"typescript-eslint": "^8.62.0",
"webpack": "^5.107.2"
"webpack": "^5.108.2"
},
"browserslist": {
"production": [
@@ -128,14 +117,16 @@
"react-redux": "^9.2.0",
"@reduxjs/toolkit": "^2.5.0",
"baseline-browser-mapping": "^2.9.19",
"swagger-client": "3.37.3",
"lodash": "4.18.1",
"lodash-es": "4.18.1",
"yaml": "1.10.3",
"uuid": "11.1.1",
"serialize-javascript": "7.0.5",
"d3-color": "3.1.0",
"ws": "^8.21.0"
"ws": "^8.21.0",
"@docusaurus/core/@docusaurus/utils/gray-matter/js-yaml": "^3.15.0",
"@docusaurus/core/**/js-yaml": "^4.3.0",
"docusaurus-plugin-openapi-docs/**/js-yaml": "^4.3.0"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}
+1 -1
View File
@@ -176,7 +176,7 @@ export interface DatabaseInfo {
expressions_in_orderby?: boolean;
// Platform features
limit_method?: string;
limit_method?: number;
limit_clause?: boolean;
max_column_name?: number;
supports_file_upload?: boolean;
+2 -1
View File
@@ -19,6 +19,7 @@
import Layout from '@theme/Layout';
import { Avatar, Card, Col, Collapse, Row, Typography } from 'antd';
import { load } from 'js-yaml';
import BlurredSection from '../components/BlurredSection';
import SectionHeader from '../components/SectionHeader';
import DataSet from '../../../RESOURCES/INTHEWILD.yaml';
@@ -36,7 +37,7 @@ interface DataSetType {
categories: Record<string, Organization[]>;
}
const typedDataSet = DataSet as DataSetType;
const typedDataSet = load(DataSet) as DataSetType;
const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
if (!contributors?.length) return null;
+2 -1
View File
@@ -22,6 +22,7 @@ import Link from '@docusaurus/Link';
import { Card, Carousel, Flex } from 'antd';
import styled from '@emotion/styled';
import GitHubButton from 'react-github-btn';
import { load } from 'js-yaml';
import { mq } from '../utils';
import SectionHeader from '../components/SectionHeader';
import databaseData from '../data/databases.json';
@@ -60,7 +61,7 @@ interface DataSetType {
categories: Record<string, Organization[]>;
}
const typedDataSet = DataSet as DataSetType;
const typedDataSet = load(DataSet) as DataSetType;
// Extract all organizations with logos for the carousel
const companiesWithLogos = Object.values(typedDataSet.categories)
+2 -2
View File
@@ -18,11 +18,11 @@
*/
declare module '*.yaml' {
const content: unknown;
const content: string;
export default content;
}
declare module '*.yml' {
const content: unknown;
const content: string;
export default content;
}
+2 -56
View File
@@ -61,10 +61,10 @@ export default function webpackExtendPlugin(): Plugin<void> {
);
});
// Add YAML loader rule directly to existing rules
// Load YAML file as raw text string
config.module?.rules?.push({
test: /\.ya?ml$/,
use: 'js-yaml-loader',
type: 'asset/source',
});
// Add swc-loader rule for superset-frontend files
@@ -168,60 +168,6 @@ export default function webpackExtendPlugin(): Plugin<void> {
__dirname,
'../../superset-frontend/packages/superset-core/src',
),
// Add proper Storybook aliases
'@storybook/blocks': path.resolve(
__dirname,
'../node_modules/@storybook/blocks',
),
'@storybook/components': path.resolve(
__dirname,
'../node_modules/@storybook/components',
),
'@storybook/theming': path.resolve(
__dirname,
'../node_modules/@storybook/theming',
),
'@storybook/client-logger': path.resolve(
__dirname,
'../node_modules/@storybook/client-logger',
),
'@storybook/core-events': path.resolve(
__dirname,
'../node_modules/@storybook/core-events',
),
// Add internal Storybook aliases
'storybook/internal/components': path.resolve(
__dirname,
'../node_modules/@storybook/components',
),
'storybook/internal/theming': path.resolve(
__dirname,
'../node_modules/@storybook/theming',
),
'storybook/internal/client-logger': path.resolve(
__dirname,
'../node_modules/@storybook/client-logger',
),
'storybook/internal/csf': path.resolve(
__dirname,
'../node_modules/@storybook/csf',
),
'storybook/internal/preview-api': path.resolve(
__dirname,
'../node_modules/@storybook/preview-api',
),
'storybook/internal/docs-tools': path.resolve(
__dirname,
'../node_modules/@storybook/docs-tools',
),
'storybook/internal/core-events': path.resolve(
__dirname,
'../node_modules/@storybook/core-events',
),
'storybook/internal/channels': path.resolve(
__dirname,
'../node_modules/@storybook/channels',
),
},
},
};
+8 -2
View File
@@ -87,6 +87,12 @@
"lifecycle": "development",
"description": "Enable semantic layers and show semantic views alongside datasets"
},
{
"name": "SOFT_DELETE",
"default": false,
"lifecycle": "development",
"description": "Temporary rollout / kill-switch gate for soft delete (default off = legacy hard delete). An emergency stop, not a clean rollback: flipping ON->OFF resurrects already-soft-deleted rows. Removed (along with its two gate points \u2014 BaseDAO.delete routing and the do_orm_execute visibility listener) once soft delete is stable."
},
{
"name": "TABLE_V2_TIME_COMPARISON_ENABLED",
"default": false,
@@ -116,9 +122,9 @@
},
{
"name": "ALERT_REPORT_SLACK_V2",
"default": false,
"default": true,
"lifecycle": "testing",
"description": "Enables Slack V2 integration for Alerts and Reports"
"description": "Enables Slack V2 integration for Alerts and Reports. Defaults to True; the legacy Slack v1 path is deprecated and will be removed in the next major release. Operators must grant the Slack bot both the `channels:read` and `groups:read` scopes so existing v1 recipients can be auto-upgraded on their next send. Without those scopes, file uploads fail (Slack retired the `files.upload` endpoint in 2025) and only text-only `chat_postMessage` sends will continue to work via the legacy path."
},
{
"name": "ALERT_REPORT_WEBHOOK",
-1
View File
@@ -30,7 +30,6 @@
"exclude": [
"node_modules",
"../superset-frontend/**/*",
"src/webpack.extend.ts",
"src/shims/**"
]
}
@@ -519,6 +519,80 @@ For a connection to a SQL endpoint you need to use the HTTP path from the endpoi
{"connect_args": {"http_path": "/sql/1.0/endpoints/****", "driver_path": "/path/to/odbc/driver"}}
```
##### OAuth2 Authentication
Superset supports OAuth2 authentication for Databricks, allowing users to authenticate with their personal Databricks accounts instead of using shared access tokens. This provides better security and audit capabilities.
###### Prerequisites
1. Create an OAuth2 application in your Databricks account:
- Go to your Databricks account console
- Navigate to **Settings** → **Developer** → **OAuth apps**
- Create a new OAuth app with the redirect URI: `http://your-superset-host:port/api/v1/database/oauth2/`
2. Configure OAuth2 in your `superset_config.py`:
```python
from datetime import timedelta
# OAuth2 configuration for Databricks
# The authorization endpoint is derived from your Databricks workspace host; the
# token endpoint must be set explicitly (see notes below).
DATABASE_OAUTH2_CLIENTS = {
"Databricks (legacy)": {
"id": "your-databricks-client-id",
"secret": "your-databricks-client-secret",
"scope": "sql",
"token_request_uri": "https://your-workspace-host/oidc/v1/token",
},
"Databricks": {
"id": "your-databricks-client-id",
"secret": "your-databricks-client-secret",
"scope": "sql",
"token_request_uri": "https://your-workspace-host/oidc/v1/token",
},
}
# OAuth2 redirect URI (adjust hostname/port for your setup)
DATABASE_OAUTH2_REDIRECT_URI = "http://your-superset-host:port/api/v1/database/oauth2/"
# Optional: OAuth2 timeout
DATABASE_OAUTH2_TIMEOUT = timedelta(seconds=30)
```
Replace the following placeholders:
- `your-databricks-client-id`: Your Databricks OAuth2 application client ID
- `your-databricks-client-secret`: Your Databricks OAuth2 application client secret
- `your-superset-host:port`: Your Superset instance hostname and port
**Multi-Cloud Provider Support**
Databricks fronts the user-to-machine (U2M) OAuth2 flow on every workspace at
`https://<workspace-host>/oidc/v1/authorize` and
`https://<workspace-host>/oidc/v1/token`, regardless of whether the workspace
runs on AWS, Azure, or GCP. Superset derives the **authorization** endpoint
directly from your connection's host, so no cloud provider or account/tenant
identifier needs to be configured.
The **token** endpoint cannot be auto-derived (token exchange has no database
context to read the host), so you must supply `token_request_uri` in
`DATABASE_OAUTH2_CLIENTS`, set to `https://<workspace-host>/oidc/v1/token` for
your workspace.
If you supply a fully-resolved `authorization_request_uri` (and/or
`token_request_uri`), those values take precedence over the host-derived
defaults.
###### Usage
Once configured, users can:
1. Connect to Databricks databases normally using access tokens
2. When querying data, Superset will automatically redirect users to authenticate with Databricks if needed
3. User-specific OAuth2 tokens will be used for database connections, providing better security and audit trails
This feature works with both "Databricks (legacy)" and "Databricks" engine types and automatically supports all major cloud providers (AWS, Azure, GCP).
#### Denodo
The recommended connector library for Denodo is
+4846 -4114
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.18.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.19.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27
+16 -1
View File
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.18.0](https://img.shields.io/badge/Version-0.18.0-informational?style=flat-square)
![Version: 0.19.0](https://img.shields.io/badge/Version-0.19.0-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -46,6 +46,21 @@ It should be a long random bytes or str.
On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverrides.secrets`
## Upgrade Notes
### Kubernetes recommended labels (breaking)
This chart labels and selects workloads using the [Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) (`app.kubernetes.io/*`) instead of the legacy `app`/`release` labels. A Deployment's `spec.selector.matchLabels` is immutable, so `helm upgrade` against a release created before this change fails with a `field is immutable` error.
To upgrade an existing release, delete the affected workloads first (their selector labels changed), then upgrade so they are recreated:
```console
kubectl delete deployment,statefulset -l release=<release-name> -n <namespace>
helm upgrade <release-name> superset/superset
```
Alternatively, perform a fresh install. This is a one-time migration; subsequent upgrades are unaffected.
## Requirements
| Repository | Name | Version |
+15
View File
@@ -45,6 +45,21 @@ It should be a long random bytes or str.
On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverrides.secrets`
## Upgrade Notes
### Kubernetes recommended labels (breaking)
This chart labels and selects workloads using the [Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) (`app.kubernetes.io/*`) instead of the legacy `app`/`release` labels. A Deployment's `spec.selector.matchLabels` is immutable, so `helm upgrade` against a release created before this change fails with a `field is immutable` error.
To upgrade an existing release, delete the affected workloads first (their selector labels changed), then upgrade so they are recreated:
```console
kubectl delete deployment,statefulset -l release=<release-name> -n <namespace>
helm upgrade <release-name> superset/superset
```
Alternatively, perform a fresh install. This is a one-time migration; subsequent upgrades are unaffected.
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}
+61 -13
View File
@@ -61,6 +61,49 @@ Create chart name and version as used by the chart label.
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels for all resources - follows Kubernetes recommended labels
https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
*/}}
{{- define "superset.labels" -}}
helm.sh/chart: {{ include "superset.chart" . }}
{{ include "superset.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: superset
{{- if .Values.extraLabels }}
{{ toYaml .Values.extraLabels }}
{{- end }}
{{- end -}}
{{/*
Selector labels - used by selectors and matchLabels
*/}}
{{- define "superset.selectorLabels" -}}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
Component labels - extends superset.labels with component-specific labels
Usage: {{ include "superset.componentLabels" (dict "component" "web" "root" .) }}
*/}}
{{- define "superset.componentLabels" -}}
{{ include "superset.labels" .root }}
app.kubernetes.io/component: {{ .component }}
{{- end -}}
{{/*
Component selector labels - for matchLabels with component
Usage: {{ include "superset.componentSelectorLabels" (dict "component" "web" "root" .) }}
*/}}
{{- define "superset.componentSelectorLabels" -}}
{{ include "superset.selectorLabels" .root }}
app.kubernetes.io/component: {{ .component }}
{{- end -}}
{{- define "superset-config" }}
import os
@@ -146,27 +189,32 @@ RESULTS_BACKEND = RedisCache(
{{- end }}
{{- define "supersetNode.selectorLabels" -}}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: web
{{- end }}
{{- define "supersetCeleryBeat.selectorLabels" -}}
app: {{ include "superset.name" . }}-celerybeat
release: {{ .Release.Name }}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: celerybeat
{{- end }}
{{- define "supersetCeleryFlower.selectorLabels" -}}
app: {{ include "superset.name" . }}-flower
release: {{ .Release.Name }}
{{- end }}
{{- define "supersetNode.selectorLabels" -}}
app: {{ include "superset.name" . }}
release: {{ .Release.Name }}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: flower
{{- end }}
{{- define "supersetWebsockets.selectorLabels" -}}
app: {{ include "superset.name" . }}-ws
release: {{ .Release.Name }}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: websocket
{{- end }}
{{- define "supersetWorker.selectorLabels" -}}
app: {{ include "superset.name" . }}-worker
release: {{ .Release.Name }}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: worker
{{- end }}
@@ -24,13 +24,7 @@ metadata:
name: {{ template "superset.fullname" . }}-extra-config
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.labels" . | nindent 4 }}
data:
{{- range $path, $config := .Values.extraConfigs }}
{{ $path }}: |
+2 -12
View File
@@ -24,13 +24,7 @@ metadata:
name: {{ template "superset.fullname" . }}-celerybeat
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.name" . }}-celerybeat
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "celerybeat" "root" .) | nindent 4 }}
{{- if .Values.supersetCeleryBeat.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetCeleryBeat.deploymentAnnotations | nindent 4 }}
{{- end }}
@@ -59,11 +53,7 @@ spec:
{{- toYaml .Values.supersetCeleryBeat.podAnnotations | nindent 8 }}
{{- end }}
labels:
app: "{{ template "superset.name" . }}-celerybeat"
release: {{ .Release.Name }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- include "supersetCeleryBeat.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetCeleryBeat.podLabels }}
{{- toYaml .Values.supersetCeleryBeat.podLabels | nindent 8 }}
{{- end }}
+2 -12
View File
@@ -24,13 +24,7 @@ metadata:
name: {{ template "superset.fullname" . }}-flower
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.name" . }}-flower
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "flower" "root" .) | nindent 4 }}
{{- if .Values.supersetCeleryFlower.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetCeleryFlower.deploymentAnnotations | nindent 4 }}
{{- end }}
@@ -48,11 +42,7 @@ spec:
{{- toYaml .Values.supersetCeleryFlower.podAnnotations | nindent 8 }}
{{- end }}
labels:
app: "{{ template "superset.name" . }}-flower"
release: {{ .Release.Name }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- include "supersetCeleryFlower.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetCeleryFlower.podLabels }}
{{- toYaml .Values.supersetCeleryFlower.podLabels | nindent 8 }}
{{- end }}
+4 -14
View File
@@ -23,15 +23,9 @@ metadata:
name: {{ template "superset.fullname" . }}-worker
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.name" . }}-worker
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.supersetWorker.deploymentLabels }}
{{- toYaml .Values.supersetWorker.deploymentLabels | nindent 4 }}
{{- include "superset.componentLabels" (dict "component" "worker" "root" .) | nindent 4 }}
{{- with .Values.supersetWorker.deploymentLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if .Values.supersetWorker.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetWorker.deploymentAnnotations | nindent 4 }}
@@ -65,11 +59,7 @@ spec:
{{- toYaml .Values.supersetWorker.podAnnotations | nindent 8 }}
{{- end }}
labels:
app: {{ template "superset.name" . }}-worker
release: {{ .Release.Name }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- include "supersetWorker.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetWorker.podLabels }}
{{- toYaml .Values.supersetWorker.podLabels | nindent 8 }}
{{- end }}
+2 -12
View File
@@ -24,13 +24,7 @@ metadata:
name: "{{ template "superset.fullname" . }}-ws"
namespace: {{ .Release.Namespace }}
labels:
app: "{{ template "superset.name" . }}-ws"
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "websocket" "root" .) | nindent 4 }}
{{- if .Values.supersetWebsockets.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetWebsockets.deploymentAnnotations | nindent 4 }}
{{- end }}
@@ -51,11 +45,7 @@ spec:
{{- toYaml .Values.supersetWebsockets.podAnnotations | nindent 8 }}
{{- end }}
labels:
app: "{{ template "superset.name" . }}-ws"
release: {{ .Release.Name }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- include "supersetWebsockets.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetWebsockets.podLabels }}
{{- toYaml .Values.supersetWebsockets.podLabels | nindent 8 }}
{{- end }}
+4 -14
View File
@@ -23,15 +23,9 @@ metadata:
name: {{ template "superset.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.supersetNode.deploymentLabels }}
{{- toYaml .Values.supersetNode.deploymentLabels | nindent 4 }}
{{- include "superset.componentLabels" (dict "component" "web" "root" .) | nindent 4 }}
{{- with .Values.supersetNode.deploymentLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if .Values.supersetNode.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetNode.deploymentAnnotations | nindent 4 }}
@@ -67,11 +61,7 @@ spec:
{{- toYaml .Values.supersetNode.podAnnotations | nindent 8 }}
{{- end }}
labels:
app: {{ template "superset.name" . }}
release: {{ .Release.Name }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- include "supersetNode.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetNode.podLabels }}
{{- toYaml .Values.supersetNode.podLabels | nindent 8 }}
{{- end }}
+1 -7
View File
@@ -23,13 +23,7 @@ kind: HorizontalPodAutoscaler
metadata:
name: {{ include "superset.fullname" . }}-hpa
labels:
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
+1 -7
View File
@@ -23,13 +23,7 @@ kind: HorizontalPodAutoscaler
metadata:
name: {{ include "superset.fullname" . }}-hpa-worker
labels:
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
+1 -7
View File
@@ -25,13 +25,7 @@ metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "ingress" "root" .) | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
+2 -10
View File
@@ -24,13 +24,7 @@ metadata:
name: {{ template "superset.fullname" . }}-init-db
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "init" "root" .) | nindent 4 }}
{{- if .Values.init.jobAnnotations }}
annotations: {{- toYaml .Values.init.jobAnnotations | nindent 4 }}
{{- end }}
@@ -42,9 +36,7 @@ spec:
annotations: {{- toYaml .Values.init.podAnnotations | nindent 8 }}
{{- end }}
labels:
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
{{- include "superset.componentSelectorLabels" (dict "component" "init" "root" .) | nindent 8 }}
job: {{ template "superset.fullname" . }}-init-db
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
+1 -7
View File
@@ -27,13 +27,7 @@ kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-celerybeat-pdb
labels:
app: {{ template "superset.name" $ }}-celerybeat
chart: {{ template "superset.chart" $ }}
release: {{ $.Release.Name }}
heritage: {{ $.Release.Service }}
{{- if $.Values.extraLabels }}
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "celerybeat" "root" $) | nindent 4 }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
+1 -7
View File
@@ -27,13 +27,7 @@ kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-flower-pdb
labels:
app: {{ template "superset.name" $ }}-flower
chart: {{ template "superset.chart" $ }}
release: {{ $.Release.Name }}
heritage: {{ $.Release.Service }}
{{- if $.Values.extraLabels }}
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "flower" "root" $) | nindent 4 }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
+1 -7
View File
@@ -27,13 +27,7 @@ kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-worker-pdb
labels:
app: {{ template "superset.name" $ }}-worker
chart: {{ template "superset.chart" $ }}
release: {{ $.Release.Name }}
heritage: {{ $.Release.Service }}
{{- if $.Values.extraLabels }}
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "worker" "root" $) | nindent 4 }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
+1 -7
View File
@@ -27,13 +27,7 @@ kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-ws-pdb
labels:
app: {{ template "superset.name" $ }}-ws
chart: {{ template "superset.chart" $ }}
release: {{ $.Release.Name }}
heritage: {{ $.Release.Service }}
{{- if $.Values.extraLabels }}
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "websocket" "root" $) | nindent 4 }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
+1 -7
View File
@@ -27,13 +27,7 @@ kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-pdb
labels:
app: {{ template "superset.name" $ }}
chart: {{ template "superset.chart" $ }}
release: {{ $.Release.Name }}
heritage: {{ $.Release.Service }}
{{- if $.Values.extraLabels }}
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "web" "root" $) | nindent 4 }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
+1 -7
View File
@@ -23,13 +23,7 @@ metadata:
name: {{ template "superset.fullname" . }}-env
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.fullname" . }}
chart: {{ template "superset.chart" . }}
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.labels" . | nindent 4 }}
type: Opaque
stringData:
REDIS_HOST: {{ tpl .Values.supersetNode.connections.redis_host . | quote }}
@@ -23,13 +23,7 @@ metadata:
name: {{ template "superset.fullname" . }}-config
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.fullname" . }}
chart: {{ template "superset.chart" . }}
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.labels" . | nindent 4 }}
type: Opaque
stringData:
superset_config.py: |
+1 -7
View File
@@ -24,13 +24,7 @@ metadata:
name: "{{ template "superset.fullname" . }}-ws-config"
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.fullname" . }}
chart: {{ template "superset.chart" . }}
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.labels" . | nindent 4 }}
type: Opaque
stringData:
config.json: |
+2 -9
View File
@@ -24,13 +24,7 @@ metadata:
name: "{{ template "superset.fullname" . }}-flower"
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "flower" "root" .) | nindent 4 }}
{{- with .Values.supersetCeleryFlower.service.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
@@ -45,8 +39,7 @@ spec:
nodePort: {{ .Values.supersetCeleryFlower.service.nodePort.http }}
{{- end }}
selector:
app: {{ template "superset.name" . }}-flower
release: {{ .Release.Name }}
{{- include "supersetCeleryFlower.selectorLabels" . | nindent 4 }}
{{- if .Values.supersetCeleryFlower.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.supersetCeleryFlower.service.loadBalancerIP }}
{{- end }}
+2 -9
View File
@@ -24,13 +24,7 @@ metadata:
name: "{{ template "superset.fullname" . }}-ws"
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "websocket" "root" .) | nindent 4 }}
{{- with .Values.supersetWebsockets.service.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
@@ -45,8 +39,7 @@ spec:
nodePort: {{ .Values.supersetWebsockets.service.nodePort.http }}
{{- end }}
selector:
app: "{{ template "superset.name" . }}-ws"
release: {{ .Release.Name }}
{{- include "supersetWebsockets.selectorLabels" . | nindent 4 }}
{{- if .Values.supersetWebsockets.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.supersetWebsockets.service.loadBalancerIP }}
{{- end }}
+2 -9
View File
@@ -23,13 +23,7 @@ metadata:
name: {{ template "superset.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- include "superset.componentLabels" (dict "component" "web" "root" .) | nindent 4 }}
{{- with .Values.service.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
@@ -44,8 +38,7 @@ spec:
nodePort: {{ .Values.service.nodePort.http }}
{{- end }}
selector:
app: {{ template "superset.name" . }}
release: {{ .Release.Name }}
{{- include "supersetNode.selectorLabels" . | nindent 4 }}
{{- if .Values.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}
+1 -7
View File
@@ -24,17 +24,11 @@ metadata:
name: {{ include "superset.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: {{ include "superset.name" . }}
helm.sh/chart: {{ include "superset.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- include "superset.labels" . | nindent 4 }}
{{- if semverCompare "> 1.6" .Capabilities.KubeVersion.GitVersion }}
kubernetes.io/cluster-service: "true"
{{- end }}
addonmanager.kubernetes.io/mode: Reconcile
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.serviceAccount.annotations }}
annotations: {{- toYaml .Values.serviceAccount.annotations | nindent 4 }}
{{- end }}
+520
View File
@@ -0,0 +1,520 @@
#
# 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.
#
suite: Label Consistency Tests
templates:
- deployment.yaml
- deployment-worker.yaml
- deployment-beat.yaml
- deployment-flower.yaml
- deployment-ws.yaml
- service.yaml
- service-ws.yaml
- service-flower.yaml
- init-job.yaml
- ingress.yaml
- configmap-superset.yaml
- secret-superset-config.yaml
- secret-ws.yaml
- pdb.yaml
- pdb-worker.yaml
- pdb-beat.yaml
- pdb-flower.yaml
- pdb-ws.yaml
# These tests validate that Kubernetes recommended labels are consistently applied
# across all chart resources per https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
#
# Required Labels (app.kubernetes.io/):
# - name: The name of the application
# - instance: A unique name identifying the instance of an application
# - version: The current version of the application
# - component: The component within the architecture
# - part-of: The name of a higher level application this one is part of
# - managed-by: The tool being used to manage the operation of an application
#
# Helm-specific Labels:
# - helm.sh/chart: The chart name and version
tests:
# =============================================================================
# Main Deployment Labels
# =============================================================================
- it: should have all recommended labels on main deployment
template: deployment.yaml
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/version"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/managed-by"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/part-of"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- isNotNull:
path: metadata.labels["helm.sh/chart"]
- it: should have correct component label on main deployment
template: deployment.yaml
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: web
- it: should have part-of label set to superset on main deployment
template: deployment.yaml
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/part-of"]
value: superset
# =============================================================================
# Worker Deployment Labels
# =============================================================================
- it: should have all recommended labels on worker deployment
template: deployment-worker.yaml
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/version"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/managed-by"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/part-of"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on worker deployment
template: deployment-worker.yaml
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: worker
# =============================================================================
# Celery Beat Deployment Labels
# =============================================================================
- it: should have all recommended labels on celerybeat deployment
template: deployment-beat.yaml
set:
supersetCeleryBeat.enabled: true
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on celerybeat deployment
template: deployment-beat.yaml
set:
supersetCeleryBeat.enabled: true
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: celerybeat
# =============================================================================
# Flower Deployment Labels
# =============================================================================
- it: should have all recommended labels on flower deployment
template: deployment-flower.yaml
set:
supersetCeleryFlower.enabled: true
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on flower deployment
template: deployment-flower.yaml
set:
supersetCeleryFlower.enabled: true
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: flower
# =============================================================================
# WebSocket Deployment Labels
# =============================================================================
- it: should have all recommended labels on websocket deployment
template: deployment-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on websocket deployment
template: deployment-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: websocket
# =============================================================================
# Service Labels
# =============================================================================
- it: should have all recommended labels on main service
template: service.yaml
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on main service
template: service.yaml
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: web
- it: should have all recommended labels on websocket service
template: service-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on websocket service
template: service-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: websocket
- it: should have all recommended labels on flower service
template: service-flower.yaml
set:
supersetCeleryFlower.enabled: true
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on flower service
template: service-flower.yaml
set:
supersetCeleryFlower.enabled: true
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: flower
# =============================================================================
# Init Job Labels
# =============================================================================
- it: should have all recommended labels on init job
template: init-job.yaml
set:
init.enabled: true
init.createAdmin: true
init.adminUser.password: "test-password"
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on init job
template: init-job.yaml
set:
init.enabled: true
init.createAdmin: true
init.adminUser.password: "test-password"
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: init
# =============================================================================
# Ingress Labels
# =============================================================================
- it: should have all recommended labels on ingress
template: ingress.yaml
set:
ingress.enabled: true
ingress.hosts:
- host: superset.example.com
paths:
- path: /
pathType: Prefix
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on ingress
template: ingress.yaml
set:
ingress.enabled: true
ingress.hosts:
- host: superset.example.com
paths:
- path: /
pathType: Prefix
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: ingress
# =============================================================================
# Selector Label Consistency
#
# These use value assertions (not isNotNull) on purpose: a missing/misscoped
# release name renders as the string "<no value>", which is non-null and would
# silently pass isNotNull. Asserting the concrete value catches that class of
# bug, and asserting the pod template labels equal the selector guards the
# immutable spec.selector.matchLabels <-> pod label invariant.
# =============================================================================
- it: should set selector matchLabels to concrete values on main deployment
template: deployment.yaml
asserts:
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/name"]
value: superset
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: web
- it: should match pod template labels to the selector on main deployment
template: deployment.yaml
asserts:
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/name"]
value: superset
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/component"]
value: web
- it: should set selector matchLabels to concrete values on worker deployment
template: deployment-worker.yaml
asserts:
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: worker
- it: should match pod template labels to the selector on worker deployment
template: deployment-worker.yaml
asserts:
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/component"]
value: worker
# =============================================================================
# Extra Labels Support
# =============================================================================
- it: should include extraLabels when specified
template: deployment.yaml
set:
extraLabels:
custom-label: custom-value
environment: production
asserts:
- equal:
path: metadata.labels.custom-label
value: custom-value
- equal:
path: metadata.labels.environment
value: production
- it: should include extraLabels in service
template: service.yaml
set:
extraLabels:
custom-label: custom-value
asserts:
- equal:
path: metadata.labels.custom-label
value: custom-value
# =============================================================================
# ConfigMap / Secret Labels
# =============================================================================
- it: should have recommended labels on extra-config configmap
template: configmap-superset.yaml
set:
extraConfigs:
custom.py: "FOO = 1"
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- equal:
path: metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- isNotNull:
path: metadata.labels["app.kubernetes.io/managed-by"]
- equal:
path: metadata.labels["app.kubernetes.io/part-of"]
value: superset
- it: should have recommended labels on superset config secret
template: secret-superset-config.yaml
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- equal:
path: metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: metadata.labels["app.kubernetes.io/part-of"]
value: superset
- it: should have recommended labels on websocket config secret
template: secret-ws.yaml
set:
supersetWebsockets.enabled: true
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- equal:
path: metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
# =============================================================================
# PodDisruptionBudget Labels (metadata must match the selector)
# =============================================================================
- it: should have recommended labels and matching selector on main pdb
template: pdb.yaml
set:
supersetNode.podDisruptionBudget.enabled: true
supersetNode.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: web
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: web
- it: should set correct component on worker pdb
template: pdb-worker.yaml
set:
supersetWorker.podDisruptionBudget.enabled: true
supersetWorker.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: worker
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: worker
- it: should set correct component on celerybeat pdb
template: pdb-beat.yaml
set:
supersetCeleryBeat.podDisruptionBudget.enabled: true
supersetCeleryBeat.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: celerybeat
- it: should set correct component on flower pdb
template: pdb-flower.yaml
set:
supersetCeleryFlower.podDisruptionBudget.enabled: true
supersetCeleryFlower.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: flower
- it: should set correct component on websocket pdb
template: pdb-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.podDisruptionBudget.enabled: true
supersetWebsockets.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: websocket
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: websocket
- it: should use recommended labels on init job pod template
template: init-job.yaml
set:
init.enabled: true
asserts:
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/component"]
value: init
- isNotNull:
path: spec.template.metadata.labels.job
+9 -9
View File
@@ -54,7 +54,7 @@ dependencies = [
"cryptography>=48.0.0, <49.0.0",
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <4.0.0",
"flask-appbuilder>=5.2.1, <6.0.0",
"flask-appbuilder>=5.2.2, <6.0.0",
"flask-caching>=2.1.0, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
@@ -64,7 +64,7 @@ dependencies = [
"flask-wtf>=1.3.0, <2.0",
"geopy",
"greenlet<=3.5.1, >=3.5.1",
"gunicorn>=25.3.0, <26; sys_platform != 'win32'",
"gunicorn>=26.0.0, <27; sys_platform != 'win32'",
"hashids>=1.3.1, <2",
# holidays>=0.45 required for security fix
"holidays>=0.45, <1",
@@ -77,7 +77,7 @@ dependencies = [
# Flask-AppBuilder workaround. Tracking issue:
# https://github.com/apache/superset/issues/33162
"marshmallow>=3.0, <5",
"marshmallow-union>=0.1",
"marshmallow-union>=0.1.15.post1",
"msgpack>=1.2.0, <1.3",
"nh3>=0.3.5, <0.4",
"numpy>1.23.5, <2.3",
@@ -101,7 +101,7 @@ dependencies = [
"pyyaml>=6.0.3, <7.0.0",
"PyJWT>=2.4.0, <3.0",
"redis>=5.0.0, <6.0",
"rison>=2.0.0, <3.0",
"rison>=2.0.1, <3.0",
"selenium>=4.45.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
"sshtunnel>=0.4.0, <0.5",
@@ -154,7 +154,7 @@ elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
exasol = ["sqlalchemy-exasol>=2.4.0, <8.0"]
excel = ["xlrd>=2.0.2, <2.1"]
fastmcp = [
"fastmcp>=3.2.4,<4.0",
"fastmcp>=3.4.2,<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.
@@ -174,11 +174,11 @@ hive = [
]
impala = ["impyla>0.16.2, <0.23"]
kusto = ["sqlalchemy-kusto>=3.1.2, <4"]
kylin = ["kylinpy>=2.8.1, <2.9"]
kylin = ["kylinpy>=2.8.4, <2.9"]
mssql = ["pymssql>=2.3.13, <3"]
# motherduck is an alias for duckdb - MotherDuck works via the duckdb driver
motherduck = ["apache-superset[duckdb]"]
mysql = ["mysqlclient>=2.1.0, <3"]
mysql = ["mysqlclient>=2.2.8, <3"]
ocient = [
"sqlalchemy-ocient>=1.0.0",
"pyocient>=1.0.15, <4",
@@ -186,7 +186,7 @@ ocient = [
"geojson",
]
oracle = ["oracledb>=2.0.0, <5"]
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
parseable = ["sqlalchemy-parseable>=0.1.6,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.60.0, <2"]
postgres = ["psycopg2-binary==2.9.12"]
@@ -216,7 +216,7 @@ netezza = ["nzalchemy>=11.0.2, < 11.2"]
starrocks = ["starrocks>=1.3.3, <2"]
doris = ["pydoris>=1.0.0, <2.0.0"]
oceanbase = ["oceanbase_py>=0.0.1.2"]
ydb = ["ydb-sqlalchemy>=0.1.22", "ydb-sqlglot-plugin>=0.2.5"]
ydb = ["ydb-sqlalchemy>=0.1.22", "ydb-sqlglot-plugin>=0.2.8"]
development = [
# no bounds for apache-superset-extensions-cli until a stable version
"apache-superset-extensions-cli",
+4 -4
View File
@@ -122,7 +122,7 @@ flask==2.3.3
# flask-session
# flask-sqlalchemy
# flask-wtf
flask-appbuilder==5.2.1
flask-appbuilder==5.2.2
# via
# apache-superset (pyproject.toml)
# apache-superset-core
@@ -169,7 +169,7 @@ greenlet==3.5.1
# apache-superset (pyproject.toml)
# shillelagh
# sqlalchemy
gunicorn==25.3.0
gunicorn==26.0.0
# via apache-superset (pyproject.toml)
h11==0.16.0
# via wsproto
@@ -236,7 +236,7 @@ marshmallow-sqlalchemy==1.5.0
# via
# -r requirements/base.in
# flask-appbuilder
marshmallow-union==0.1.15
marshmallow-union==0.1.15.post1
# via apache-superset (pyproject.toml)
mdurl==0.1.2
# via markdown-it-py
@@ -369,7 +369,7 @@ rfc3339-validator==0.1.4
# via openapi-schema-validator
rich==13.9.4
# via flask-limiter
rison==2.0.0
rison==2.0.1
# via apache-superset (pyproject.toml)
rpds-py==0.25.0
# via
+44 -32
View File
@@ -53,7 +53,7 @@ attrs==25.3.0
# requests-cache
# trio
authlib==1.6.12
# via fastmcp
# via fastmcp-slim
babel==2.17.0
# via
# -c requirements/base-constraint.txt
@@ -184,6 +184,7 @@ cryptography==48.0.1
# apache-superset
# authlib
# google-auth
# joserfc
# paramiko
# pyjwt
# pyopenssl
@@ -191,7 +192,7 @@ cryptography==48.0.1
cycler==0.12.1
# via matplotlib
cyclopts==4.2.4
# via fastmcp
# via fastmcp-slim
db-dtypes==1.3.1
# via pandas-gbq
defusedxml==0.7.1
@@ -236,9 +237,11 @@ et-xmlfile==2.0.0
# -c requirements/base-constraint.txt
# openpyxl
exceptiongroup==1.3.0
# via fastmcp
fastmcp==3.2.4
# via fastmcp-slim
fastmcp==3.4.2
# via apache-superset
fastmcp-slim==3.4.2
# via fastmcp
filelock==3.20.3
# via
# -c requirements/base-constraint.txt
@@ -260,7 +263,7 @@ flask==2.3.3
# flask-sqlalchemy
# flask-testing
# flask-wtf
flask-appbuilder==5.2.1
flask-appbuilder==5.2.2
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -382,7 +385,7 @@ greenlet==3.5.1
# shillelagh
# sqlalchemy
griffelib==2.0.2
# via fastmcp
# via fastmcp-slim
grpcio==1.81.1
# via
# apache-superset
@@ -390,7 +393,7 @@ grpcio==1.81.1
# grpcio-status
grpcio-status==1.60.1
# via google-api-core
gunicorn==25.3.0
gunicorn==26.0.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -413,7 +416,7 @@ httpcore==1.0.9
# via httpx
httpx==0.28.1
# via
# fastmcp
# fastmcp-slim
# mcp
httpx-sse==0.4.1
# via mcp
@@ -472,12 +475,14 @@ jmespath==1.1.0
# via
# boto3
# botocore
joserfc==1.7.2
# via fastmcp-slim
jsonpath-ng==1.8.0
# via
# -c requirements/base-constraint.txt
# apache-superset
jsonref==1.1.0
# via fastmcp
# via fastmcp-slim
jsonschema==4.23.0
# via
# -c requirements/base-constraint.txt
@@ -487,7 +492,7 @@ jsonschema==4.23.0
# openapi-spec-validator
jsonschema-path==0.3.4
# via
# fastmcp
# fastmcp-slim
# openapi-spec-validator
jsonschema-specifications==2025.4.1
# via
@@ -541,7 +546,7 @@ marshmallow-sqlalchemy==1.5.0
# via
# -c requirements/base-constraint.txt
# flask-appbuilder
marshmallow-union==0.1.15
marshmallow-union==0.1.15.post1
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -550,7 +555,7 @@ matplotlib==3.9.0
mccabe==0.7.0
# via pylint
mcp==1.24.0
# via fastmcp
# via fastmcp-slim
mdurl==0.1.2
# via
# -c requirements/base-constraint.txt
@@ -567,7 +572,7 @@ msgspec==0.19.0
# via
# -c requirements/base-constraint.txt
# flask-session
mysqlclient==2.2.6
mysqlclient==2.2.8
# via apache-superset
nh3==0.3.5
# via
@@ -594,7 +599,7 @@ odfpy==1.4.1
# -c requirements/base-constraint.txt
# pandas
openapi-pydantic==0.5.1
# via fastmcp
# via fastmcp-slim
openapi-schema-validator==0.6.3
# via
# -c requirements/base-constraint.txt
@@ -606,7 +611,7 @@ openpyxl==3.1.5
# -c requirements/base-constraint.txt
# pandas
opentelemetry-api==1.39.1
# via fastmcp
# via fastmcp-slim
ordered-set==4.1.0
# via
# -c requirements/base-constraint.txt
@@ -627,7 +632,7 @@ packaging==25.0
# deprecation
# docker
# duckdb-engine
# fastmcp
# fastmcp-slim
# google-cloud-bigquery
# gunicorn
# limits
@@ -672,7 +677,7 @@ pip==25.1.1
platformdirs==4.3.8
# via
# -c requirements/base-constraint.txt
# fastmcp
# fastmcp-slim
# pylint
# requests-cache
# virtualenv
@@ -714,7 +719,7 @@ psutil==6.1.0
psycopg2-binary==2.9.12
# via apache-superset
py-key-value-aio==0.4.4
# via fastmcp
# via fastmcp-slim
pyarrow==24.0.0
# via
# -c requirements/base-constraint.txt
@@ -741,7 +746,7 @@ pydantic==2.11.7
# -c requirements/base-constraint.txt
# apache-superset
# apache-superset-core
# fastmcp
# fastmcp-slim
# mcp
# openapi-pydantic
# pydantic-settings
@@ -750,7 +755,9 @@ pydantic-core==2.33.2
# -c requirements/base-constraint.txt
# pydantic
pydantic-settings==2.10.1
# via mcp
# via
# fastmcp-slim
# mcp
pydata-google-auth==1.9.0
# via pandas-gbq
pydruid==0.6.9
@@ -793,7 +800,7 @@ pyparsing==3.2.3
# apache-superset
# matplotlib
pyperclip==1.10.0
# via fastmcp
# via fastmcp-slim
pysocks==1.7.1
# via
# -c requirements/base-constraint.txt
@@ -835,12 +842,14 @@ python-dotenv==1.2.2
# via
# -c requirements/base-constraint.txt
# apache-superset
# fastmcp
# fastmcp-slim
# pydantic-settings
python-ldap==3.4.7
# via apache-superset
python-multipart==0.0.29
# via mcp
# via
# fastmcp-slim
# mcp
pytz==2025.2
# via
# -c requirements/base-constraint.txt
@@ -856,7 +865,7 @@ pyyaml==6.0.3
# -c requirements/base-constraint.txt
# apache-superset
# apispec
# fastmcp
# fastmcp-slim
# jsonschema-path
# pre-commit
redis==5.3.1
@@ -899,12 +908,12 @@ rich==13.9.4
# via
# -c requirements/base-constraint.txt
# cyclopts
# fastmcp
# fastmcp-slim
# flask-limiter
# rich-rst
rich-rst==1.3.1
# via cyclopts
rison==2.0.0
rison==2.0.1
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -995,8 +1004,10 @@ sshtunnel==0.4.0
# via
# -c requirements/base-constraint.txt
# apache-superset
starlette==0.49.1
# via mcp
starlette==1.3.1
# via
# fastmcp-slim
# mcp
statsd==4.0.1
# via apache-superset
syntaqlite==0.4.2
@@ -1035,6 +1046,7 @@ typing-extensions==4.15.0
# apache-superset-core
# cattrs
# exceptiongroup
# fastmcp-slim
# grpcio
# limits
# mcp
@@ -1062,7 +1074,7 @@ tzdata==2025.2
tzlocal==5.2
# via trino
uncalled-for==0.2.0
# via fastmcp
# via fastmcp-slim
url-normalize==2.2.1
# via
# -c requirements/base-constraint.txt
@@ -1077,7 +1089,7 @@ urllib3==2.7.0
# selenium
uvicorn==0.37.0
# via
# fastmcp
# fastmcp-slim
# mcp
vine==5.1.0
# via
@@ -1093,7 +1105,7 @@ watchdog==6.0.0
# apache-superset
# apache-superset-extensions-cli
watchfiles==1.1.1
# via fastmcp
# via fastmcp-slim
wcwidth==0.2.13
# via
# -c requirements/base-constraint.txt
@@ -1103,7 +1115,7 @@ websocket-client==1.8.0
# -c requirements/base-constraint.txt
# selenium
websockets==15.0.1
# via fastmcp
# via fastmcp-slim
werkzeug==3.1.6
# via
# -c requirements/base-constraint.txt
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# 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.
"""Stamp do-not-translate msgids in a .pot with an extracted-comment marker.
For every msgid listed in ``superset/translations/do-not-translate.txt`` that is
present in the target .pot, add a ``#. do-not-translate`` extracted
comment. gettext extracted comments (``#.``) propagate from the .pot into every
language .po on ``pybabel update``, so the do-not-translate status stays
consistent across all catalogs from a single registry.
Run from ``babel_update.sh`` after the .pot is extracted and normalized (and
before ``pybabel update``). Idempotent: re-running makes no further changes.
Usage:
python scripts/translations/apply_do_not_translate.py [POT_PATH]
# POT_PATH defaults to superset/translations/messages.pot
"""
from __future__ import annotations
import sys
from pathlib import Path
# The standardized extracted-comment marker. Kept in sync with backfill_po.py.
MARKER: str = "do-not-translate"
_MARKER_LINE: str = f"#. {MARKER}"
TRANSLATIONS_DIR: Path = (
Path(__file__).parent.parent.parent / "superset" / "translations"
)
DEFAULT_POT: Path = TRANSLATIONS_DIR / "messages.pot"
REGISTRY: Path = TRANSLATIONS_DIR / "do-not-translate.txt"
def load_registry(path: Path = REGISTRY) -> set[str]:
"""Return the set of do-not-translate msgids (skips comments/blank lines).
Each line is stripped before the blank/comment check, so trailing
whitespace or an indented comment never yields a msgid that fails to match
the .pot.
"""
if not path.exists():
return set()
entries: set[str] = set()
for raw_line in path.read_text(encoding="utf-8").splitlines():
line: str = raw_line.strip()
if line and not line.startswith("#"):
entries.add(line)
return entries
def _escape(msgid: str) -> str:
"""Escape a msgid the way gettext writes it on a `msgid "..."` line."""
return msgid.replace("\\", "\\\\").replace('"', '\\"')
def apply_markers(pot_path: Path, registry: set[str]) -> int:
"""Insert the marker comment above each registry msgid via text edit.
Text manipulation (rather than a polib round-trip) preserves the .pot's
exact wrapping/layout, so the only change is the added marker lines.
Idempotent. Returns the number of entries newly marked.
"""
lines: list[str] = pot_path.read_text(encoding="utf-8").split("\n")
targets: set[str] = {f'msgid "{_escape(m)}"' for m in registry}
out: list[str] = []
changed: int = 0
for line in lines:
if line in targets and (not out or out[-1] != _MARKER_LINE):
# `#.` extracted comments precede `msgid`; these registry entries are
# bare single-line msgids, so inserting directly above is correct.
out.append(_MARKER_LINE)
changed += 1
out.append(line)
if changed:
pot_path.write_text("\n".join(out), encoding="utf-8")
return changed
def main() -> None:
"""Stamp the marker onto the target .pot from the registry."""
pot_path: Path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_POT
if not pot_path.exists():
print(f"POT file not found: {pot_path}", file=sys.stderr)
sys.exit(1)
# Fail fast if the registry file is absent: babel_update.sh depends on this
# step to stamp the .pot, and continuing would silently publish catalogs
# without any do-not-translate markers. An existing-but-empty registry is a
# valid state (nothing to mark), so only a missing file is an error.
if not REGISTRY.exists():
print(
f"do-not-translate registry not found at {REGISTRY}; refusing to "
"produce unmarked translation artifacts.",
file=sys.stderr,
)
sys.exit(1)
registry: set[str] = load_registry()
if not registry:
print(
f"do-not-translate registry {REGISTRY} is empty; nothing to mark.",
file=sys.stderr,
)
return
changed: int = apply_markers(pot_path, registry)
print(
f"do-not-translate: marked {changed} new entr(y/ies) with {MARKER} "
f"in {pot_path.name} ({len(registry)} msgids in registry).",
file=sys.stderr,
)
if __name__ == "__main__":
main()
+8
View File
@@ -55,6 +55,14 @@ msgcat --sort-by-msgid --no-wrap --no-location superset/translations/messages.po
cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \
&& mv messages.pot.tmp superset/translations/messages.pot
# Stamp do-not-translate msgids (superset/translations/do-not-translate.txt) with
# a `#. do-not-translate` extracted comment. Extracted comments
# propagate from the .pot into every catalog on the `pybabel update` below, so
# the do-not-translate status stays consistent across all languages.
# Fail fast: without this guard the script would continue past a marker-stamping
# failure and `pybabel update` would publish catalogs missing the markers.
python scripts/translations/apply_do_not_translate.py superset/translations/messages.pot || exit 1
# --no-fuzzy-matching: when a *new* source string is added, Babel's fuzzy
# matcher otherwise guesses a "close" existing translation and marks it
# `#, fuzzy` in every language catalog. Those guesses are (a) usually wrong
+161 -1
View File
@@ -91,22 +91,28 @@ _ASF_LICENSE_HEADER = """\
LANGUAGE_NAMES: dict[str, str] = {
"ar": "Arabic",
"ca": "Catalan",
"cs": "Czech",
"de": "German",
"es": "Spanish",
"fa": "Persian (Farsi)",
"fi": "Finnish",
"fr": "French",
"it": "Italian",
"ja": "Japanese",
"ko": "Korean",
"lv": "Latvian",
"mi": "Māori",
"nl": "Dutch",
"pl": "Polish",
"pt": "Portuguese",
"pt_BR": "Brazilian Portuguese",
"ro": "Romanian",
"ru": "Russian",
"sk": "Slovak",
"sl": "Slovenian",
"sr": "Serbian",
"sr_Latn": "Serbian (Latin script)",
"th": "Thai",
"tr": "Turkish",
"uk": "Ukrainian",
"zh": "Chinese (Simplified)",
@@ -146,6 +152,60 @@ def _is_missing(entry: polib.POEntry) -> bool:
return not entry.msgstr
# Canonical registry of msgids that must never be machine-translated: literal
# tokens compared against source (SQL keywords, confirmation words), enum values
# (d3 interpolation modes), icon names (e.g. "bolt" -> the ⚡ Explore control
# icon), API field names, code constants, and example placeholders. Translating
# them can break icon lookups, enum matching, or API contracts, or is simply
# meaningless (proper nouns, example values). apply_do_not_translate.py stamps
# these msgids in messages.pot with a `#. do-not-translate`
# extracted comment that propagates to every catalog on `pybabel update`.
DO_NOT_TRANSLATE_REGISTRY: Path = TRANSLATIONS_DIR / "do-not-translate.txt"
def _load_do_not_translate(path: Path = DO_NOT_TRANSLATE_REGISTRY) -> frozenset[str]:
"""Load the do-not-translate msgids (skips comment/blank lines).
Lines are stripped before the blank/comment checks, matching the parsing in
apply_do_not_translate.py, so trailing whitespace or an indented comment
never yields a msgid that fails to match a catalog entry.
"""
if not path.exists():
return frozenset()
return frozenset(
stripped
for line in path.read_text(encoding="utf-8").splitlines()
if (stripped := line.strip()) and not stripped.startswith("#")
)
DO_NOT_TRANSLATE: frozenset[str] = _load_do_not_translate()
# An explicit do-not-translate marker on an entry, matched in either the
# extracted comment (`#. do-not-translate`, the standard propagated
# from the .pot) or a translator comment (e.g. the ru catalog's legacy
# "# Не переводить"). Honored so a human's deliberate decision is never
# overridden even if a msgid is missing from the registry.
_DO_NOT_TRANSLATE_COMMENT: re.Pattern[str] = re.compile(
r"не\s+переводить|do[\s-]?not[\s-]?translate|don'?t\s+translate",
re.IGNORECASE,
)
def _is_do_not_translate(entry: polib.POEntry) -> bool:
"""Return True if an entry must be left for a human (never machine-filled).
Either its msgid is in the do-not-translate registry, or the entry carries
an explicit do-not-translate marker in its extracted or translator comment.
"""
if entry.msgid in DO_NOT_TRANSLATE:
return True
return any(
comment and _DO_NOT_TRANSLATE_COMMENT.search(comment)
for comment in (entry.comment, entry.tcomment)
)
def _context_langs(
item: dict[str, Any], index: dict[str, Any], target_lang: str
) -> list[str]:
@@ -346,6 +406,97 @@ def translate_batch(
return parse_response(result.stdout.strip(), len(batch))
def _translate_single_plaintext(
model: str,
target_lang: str,
item: dict[str, Any],
index: dict[str, Any],
) -> str | None:
"""Translate a single entry with a plain-text prompt (no JSON envelope).
Fallback for an entry whose JSON batch response cannot be parsed typically
because the source string contains literal double-quotes that the model
echoes back unescaped, corrupting the surrounding JSON. Asking for a bare
string sidesteps the JSON contract entirely. Returns the translation text,
or None if the CLI call fails.
"""
claude_bin = shutil.which("claude")
if not claude_bin:
raise RuntimeError(
"claude CLI not found. Install Claude Code or add it to PATH."
)
lines = [
"You are a professional translator specializing in software UI strings.",
f"Translate the following English string into {_lang_name(target_lang)} "
f"({target_lang}).",
"Return ONLY the translation as plain text — no surrounding quotes, no "
"JSON, no markdown fences, no explanation.",
"Preserve all format placeholders exactly (%(name)s, {name}, %s, %d), any "
"HTML tags, and any inner quotation marks.",
"",
f"English: {item['msgid']}",
]
if item.get("msgid_plural"):
lines.append(f"English plural: {item['msgid_plural']}")
refs = index.get(item["index_key"], {})
ref_lines = [
f"{_lang_name(lang)}: {val}"
for lang, val in sorted(refs.items())
if lang != target_lang and isinstance(val, str) and val
]
if ref_lines:
lines.append("")
lines.append("Reference translations in other languages:")
lines.extend(ref_lines)
prompt = "\n".join(lines)
# claude_bin is resolved via shutil.which — not user-controlled input
result = subprocess.run( # noqa: S603
[claude_bin, "--model", model, "-p"],
input=prompt,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
return None
text = result.stdout.strip()
# Strip accidental markdown fences or wrapping quotes the model may add.
text = re.sub(r"^```[^\n]*\n?", "", text)
text = re.sub(r"\n?```$", "", text).strip()
if len(text) >= 2 and text[0] == '"' and text[-1] == '"':
text = text[1:-1]
return text or None
def _resilient_translate(
model: str,
target_lang: str,
batch: list[dict[str, Any]],
index: dict[str, Any],
) -> dict[int, str]:
"""Translate a batch, isolating entries that break the JSON response contract.
``translate_batch`` sends the whole batch in one request and parses a single
JSON object back. A source string containing literal double-quotes can make
the model emit unescaped quotes, so ``json.loads`` fails and the ENTIRE batch
would be lost. To salvage the rest, on a parse failure (ValueError) we bisect
the batch and recurse; a lone entry that still fails falls back to a
plain-text prompt via ``_translate_single_plaintext``. Returned keys are
positions within ``batch``. RuntimeError (CLI failure) is left to propagate
to the caller, preserving the existing per-batch failure handling.
"""
try:
return translate_batch(model, target_lang, batch, index)
except ValueError:
if len(batch) == 1:
text = _translate_single_plaintext(model, target_lang, batch[0], index)
return {0: text} if text else {}
mid = len(batch) // 2
left = _resilient_translate(model, target_lang, batch[:mid], index)
right = _resilient_translate(model, target_lang, batch[mid:], index)
return {**left, **{k + mid: v for k, v in right.items()}}
def _apply_plural_translation(entry: polib.POEntry, translation: str) -> None:
"""Distribute a model response across the entry's plural forms.
@@ -462,7 +613,7 @@ def _process_batches(
file=sys.stderr,
)
try:
translations = translate_batch(model, lang, batch_items, index)
translations = _resilient_translate(model, lang, batch_items, index)
except (ValueError, RuntimeError) as exc:
print(f" ERROR in batch starting at {batch_start}: {exc}", file=sys.stderr)
failed_count += len(batch_entries)
@@ -550,6 +701,15 @@ def backfill(
missing: list[polib.POEntry] = [e for e in cat if e.msgid and _is_missing(e)]
print(f"Found {len(missing)} untranslated entries for '{lang}'.", file=sys.stderr)
skipped_dnt: list[polib.POEntry] = [e for e in missing if _is_do_not_translate(e)]
if skipped_dnt:
missing = [e for e in missing if not _is_do_not_translate(e)]
print(
f"Skipping {len(skipped_dnt)} do-not-translate entries (literal "
f"tokens / translator-marked); they are left untranslated.",
file=sys.stderr,
)
if min_context > 0:
before = len(missing)
missing = [
@@ -20,21 +20,31 @@ Check that source-code changes don't cause translation regressions.
What counts as a regression
---------------------------
A regression is an *existing translation that a source change invalidated*.
The check keys on the **increase in fuzzy entries** rather than a drop in the
translated count, because a count drop happens identically for a benign
*deletion* and a real *rename*, so it cannot distinguish the two whereas a
``#, fuzzy`` marker unambiguously flags a stranded translation.
A regression is an *existing translation that a source change invalidated*:
a message that was a **confirmed, non-fuzzy translation** in the baseline and
is **fuzzy** after the PR. The check keys on this per-``msgid`` transition
rather than on the aggregate count of fuzzy entries, because a bare count
cannot tell apart two changes that move the fuzzy total by the same amount:
* ``translated -> fuzzy`` a reworded source string stranded a real
translation. **This is the regression.**
* ``untranslated -> fuzzy`` an empty ``msgstr`` was filled with a fuzzy
(unconfirmed) guess, e.g. an AI backfill committed as ``#, fuzzy``. No
existing translation was lost, so this is **not** a regression and must
pass.
Keying on the per-entry transition lets a backfill PR commit fuzzy guesses for
previously-untranslated strings (the ja/fi catalog backfills) without tripping
the check, while still catching a genuine invalidation even when the same PR
also adds new strings (which a count-delta heuristic would let mask it).
Note ``babel_update.sh`` runs ``pybabel update`` with ``--no-fuzzy-matching``,
so *adding* (or renaming) a source string does **not** auto-generate a fuzzy
guess against an unrelated existing translation new strings land as cleanly
untranslated (empty ``msgstr``). This deliberately avoids the prior behaviour
where *every* PR that merely added a translatable string tripped this check on
spurious fuzzies. As a result the check now guards against ``#, fuzzy`` entries
that arrive another way e.g. a committed ``.po`` edit rather than ones the
update step synthesises. *Deleting* a string is still not a regression: with
``--ignore-obsolete`` it is simply dropped and no fuzzy is created.
untranslated (empty ``msgstr``). The fuzzies this check sees therefore arrive
another way typically a committed ``.po`` edit. *Deleting* a string is still
not a regression: with ``--ignore-obsolete`` it is simply dropped and no fuzzy
is created.
Usage
-----
@@ -127,28 +137,72 @@ def count_stats(po_file: Path) -> dict[str, int]:
}
def entry_keys(po_file: Path) -> dict[str, list[str]]:
"""Return per-``msgid`` key sets for a .po file.
``translated_keys`` lists the non-fuzzy, non-obsolete entries with a
populated ``msgstr`` confirmed translations a source reword could strand.
``fuzzy_keys`` lists the non-obsolete entries carrying the ``fuzzy`` flag
(however they arrived a committed backfill guess or a real invalidation).
A key combines ``msgctxt`` and ``msgid`` (gettext's own identity rule) so
context-disambiguated entries stay distinct. The header entry (empty
``msgid``) is ignored. The regression check compares the baseline's
``translated_keys`` against the PR's ``fuzzy_keys``: their intersection is
exactly the set of confirmed translations the PR turned fuzzy.
Raises:
OSError: if ``polib`` cannot read or parse the file. As with a msgfmt
failure, a catalog we cannot parse is surfaced rather than silently
counted as empty.
"""
import polib # type: ignore[import-untyped] # noqa: PLC0415
translated_keys: list[str] = []
fuzzy_keys: list[str] = []
for entry in polib.pofile(str(po_file)):
if entry.obsolete or not entry.msgid:
continue
key = f"{entry.msgctxt}\x04{entry.msgid}" if entry.msgctxt else entry.msgid
if "fuzzy" in entry.flags:
fuzzy_keys.append(key)
elif (
all(entry.msgstr_plural.values())
if entry.msgid_plural
else bool(entry.msgstr)
):
translated_keys.append(key)
return {"translated_keys": translated_keys, "fuzzy_keys": fuzzy_keys}
def get_counts(
translations_dir: Path,
failures: Optional[set[str]] = None,
) -> dict[str, dict[str, int]]:
) -> dict[str, dict[str, object]]:
"""Count translated/fuzzy entries for every ``.po`` file in a directory.
Each language maps to ``{"translated", "fuzzy", "translated_keys",
"fuzzy_keys"}`` aggregate counts (for the human-readable summary) plus the
per-``msgid`` key sets the regression check actually keys on.
If ``failures`` is provided, the name of each language whose ``.po`` file
is present on disk but could not be counted (msgfmt non-zero exit, or
unparseable output) is added to it. Such a language is deliberately absent
from the returned mapping but, unlike a language whose catalog was simply
deleted, it must not be mistaken for an intentional removal: a caller that
cares about the distinction (see :func:`cmd_compare`) can inspect
``failures`` and treat it as a hard error.
is present on disk but could not be counted (msgfmt non-zero exit,
unparseable output, or a polib parse error) is added to it. Such a language
is deliberately absent from the returned mapping but, unlike a language
whose catalog was simply deleted, it must not be mistaken for an intentional
removal: a caller that cares about the distinction (see :func:`cmd_compare`)
can inspect ``failures`` and treat it as a hard error.
"""
counts: dict[str, dict[str, int]] = {}
counts: dict[str, dict[str, object]] = {}
for po_file in sorted(translations_dir.glob("*/LC_MESSAGES/messages.po")):
lang = po_file.parent.parent.name
if lang in SKIP_LANGS:
continue
try:
counts[lang] = count_stats(po_file)
except (subprocess.CalledProcessError, RuntimeError) as exc:
stats: dict[str, object] = dict(count_stats(po_file))
stats.update(entry_keys(po_file))
counts[lang] = stats
except (subprocess.CalledProcessError, RuntimeError, OSError) as exc:
# A malformed .po file (msgfmt non-zero exit, or stderr we
# can't parse) is a real problem worth seeing, but it shouldn't
# take the whole regression check down with it — that would
@@ -164,42 +218,73 @@ def get_counts(
return counts
def _normalize(entry: object) -> dict[str, int]:
"""Coerce a baseline entry into ``{"translated", "fuzzy"}``.
def _normalize(entry: object) -> dict[str, object]:
"""Coerce a baseline entry into ``{"translated", "fuzzy", *_keys}``.
Tolerates the legacy baseline format where each language mapped directly to
an integer translated count (no fuzzy data); such entries contribute a
fuzzy baseline of 0.
``translated_keys``/``fuzzy_keys`` are the per-``msgid`` sets the check
keys on. They are ``None`` (not ``[]``) when the baseline predates the
per-entry format an absent set means "unknown", which routes
:func:`cmd_compare` to the coarse aggregate fallback, whereas an empty list
is a known-empty set. Legacy formats a ``{"translated", "fuzzy"}`` dict
with no key sets, or a bare integer translated count are both tolerated.
"""
if isinstance(entry, dict):
return {
"translated": int(entry.get("translated", 0)),
"fuzzy": int(entry.get("fuzzy", 0)),
"translated_keys": (
list(entry["translated_keys"]) if "translated_keys" in entry else None
),
"fuzzy_keys": (
list(entry["fuzzy_keys"]) if "fuzzy_keys" in entry else None
),
}
if isinstance(entry, int):
return {"translated": entry, "fuzzy": 0}
return {
"translated": entry,
"fuzzy": 0,
"translated_keys": None,
"fuzzy_keys": None,
}
raise TypeError(f"Unsupported baseline entry: {entry!r}")
def build_regression_report(regressions: list[tuple[str, int, int]]) -> str:
def _key_list(stats: dict[str, object], field: str) -> Optional[list[str]]:
"""Return ``stats[field]`` as a list of keys, or ``None`` if unavailable.
A missing or non-list value reads as "unknown" so the caller can fall back
to the aggregate comparison instead of treating it as an empty key set.
"""
value = stats.get(field)
return list(value) if isinstance(value, list) else None
def _count(stats: dict[str, object], field: str) -> int:
"""Return ``stats[field]`` as an int count, defaulting to 0."""
value = stats.get(field, 0)
return value if isinstance(value, int) else 0
def build_regression_report(regressions: list[tuple[str, int, int, int]]) -> str:
"""Build a markdown report for posting as a PR comment.
Each regression tuple is ``(lang, before_fuzzy, after_fuzzy)``.
Each regression tuple is ``(lang, before_fuzzy, after_fuzzy, invalidated)``
where ``invalidated`` is the number of confirmed translations the PR turned
fuzzy.
"""
rows = "\n".join(
f"| `{lang}` | {b} | {a} | +{a - b} |" for lang, b, a in regressions
)
affected = ", ".join(f"`{lang}`" for lang, _, _ in regressions)
rows = "\n".join(f"| `{lang}` | {n} |" for lang, _b, _a, n in regressions)
affected = ", ".join(f"`{lang}`" for lang, *_ in regressions)
return (
"## ⚠️ Translation Regression Detected\n\n"
f"A source change in this PR renamed or reworded strings, invalidating "
f"existing translations (they are now `#, fuzzy`) in {affected}. Please "
f"resolve the affected `.po` files before merging.\n\n"
"_Note: intentionally **deleting** a translatable string is not a "
"regression and is not flagged here — only translations invalidated by "
"a renamed/reworded source string are._\n\n"
"| Language | Fuzzy before | Fuzzy after | New |\n"
"|----------|-------------:|------------:|----:|\n"
"_Note: neither intentionally **deleting** a translatable string nor "
"filling a previously-**untranslated** entry with a fuzzy guess (e.g. an "
"AI backfill) is a regression — only a confirmed translation that a "
"renamed/reworded source string turned fuzzy is flagged here._\n\n"
"| Language | Invalidated translations |\n"
"|----------|-------------------------:|\n"
f"{rows}\n\n"
"### How to fix\n\n"
"**1. Install dependencies** (if not already set up):\n\n"
@@ -231,6 +316,41 @@ def cmd_count(translations_dir: Path) -> None:
print(json.dumps(counts, indent=2))
def _detect_regressions(
before: dict[str, dict[str, object]],
after: dict[str, dict[str, object]],
) -> list[tuple[str, int, int, int]]:
"""Return ``(lang, before_fuzzy, after_fuzzy, invalidated)`` per regressed lang.
A regression is a key in the baseline's ``translated_keys`` that is fuzzy
after the PR a confirmed translation a source reword stranded. Filling a
previously-untranslated entry with a fuzzy guess (backfill) is therefore not
flagged (its key was absent from the baseline's translated set), and neither
is deleting a string (with ``--ignore-obsolete`` it drops, creating no
fuzzy). When per-entry key data is unavailable (a legacy baseline, or a
catalog whose key set could not be read), fall back to the coarse rule: any
net increase in the aggregate fuzzy count.
"""
regressions: list[tuple[str, int, int, int]] = []
for lang, before_stats in sorted(before.items()):
after_stats = after.get(lang)
if after_stats is None:
# Catalog absent from `after`: an intentional deletion (a
# present-but-uncountable catalog was already caught by the caller).
continue
b_fuzzy = _count(before_stats, "fuzzy")
a_fuzzy = _count(after_stats, "fuzzy")
before_translated = before_stats.get("translated_keys")
after_fuzzy = _key_list(after_stats, "fuzzy_keys")
if isinstance(before_translated, list) and after_fuzzy is not None:
invalidated = set(after_fuzzy) & set(before_translated)
if invalidated:
regressions.append((lang, b_fuzzy, a_fuzzy, len(invalidated)))
elif a_fuzzy > b_fuzzy:
regressions.append((lang, b_fuzzy, a_fuzzy, a_fuzzy - b_fuzzy))
return regressions
def cmd_compare(
before_path: str,
translations_dir: Path,
@@ -259,23 +379,12 @@ def cmd_compare(
)
sys.exit(1)
# A regression is an *increase* in fuzzy entries: the PR's source diff
# renamed/reworded strings, leaving their committed translations stranded.
# A plain drop in the translated count is NOT used — deleting a string
# lowers it identically to a rename but is a legitimate change, and with
# `pybabel update --ignore-obsolete` a deletion creates no fuzzy entry.
regressions: list[tuple[str, int, int]] = []
for lang, before_stats in sorted(before.items()):
after_stats = after.get(lang, {"translated": 0, "fuzzy": 0})
if after_stats["fuzzy"] > before_stats["fuzzy"]:
regressions.append((lang, before_stats["fuzzy"], after_stats["fuzzy"]))
if regressions:
if regressions := _detect_regressions(before, after):
print("Translation regression detected!\n")
for lang, b, a in regressions:
for lang, _b, _a, n in regressions:
print(
f" {lang}: {a - b} translation(s) invalidated "
f"(fuzzy {b} -> {a}) by a renamed/reworded source string"
f" {lang}: {n} confirmed translation(s) invalidated "
f"(now fuzzy) by a renamed/reworded source string"
)
print(
"\nResolve the newly-fuzzy entries in the affected .po files "
@@ -290,14 +399,16 @@ def cmd_compare(
# All good — print a summary so it's easy to read in CI logs.
print("No translation regressions.\n")
for lang in sorted(after):
before_stats = before.get(lang, {"translated": 0, "fuzzy": 0})
before_stats: dict[str, object] = before.get(lang, {})
after_stats = after[lang]
t_delta = after_stats["translated"] - before_stats["translated"]
f_delta = after_stats["fuzzy"] - before_stats["fuzzy"]
b_translated = _count(before_stats, "translated")
a_translated = _count(after_stats, "translated")
b_fuzzy = _count(before_stats, "fuzzy")
a_fuzzy = _count(after_stats, "fuzzy")
print(
f" {lang}: translated {before_stats['translated']} -> "
f"{after_stats['translated']} ({t_delta:+d}), fuzzy "
f"{before_stats['fuzzy']} -> {after_stats['fuzzy']} ({f_delta:+d})"
f" {lang}: translated {b_translated} -> {a_translated} "
f"({a_translated - b_translated:+d}), fuzzy "
f"{b_fuzzy} -> {a_fuzzy} ({a_fuzzy - b_fuzzy:+d})"
)
+598 -814
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -37,10 +37,10 @@
"jwt-decode": "^4.0.0"
},
"devDependencies": {
"@babel/cli": "^7.25.6",
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@babel/preset-typescript": "^7.24.7",
"@babel/cli": "^7.29.7",
"@babel/core": "^7.29.7",
"@babel/preset-env": "^7.29.7",
"@babel/preset-typescript": "7.29.7",
"@types/node": "^25.4.0",
"babel-loader": "^9.1.3",
"tscw-config": "^1.1.2",
@@ -19,9 +19,8 @@
export const DASHBOARD_LIST = '/dashboard/list/';
export const CHART_LIST = '/chart/list/';
export const WORLD_HEALTH_DASHBOARD = '/superset/dashboard/world_health/';
export const SAMPLE_DASHBOARD_1 = '/superset/dashboard/1-sample-dashboard/';
export const SUPPORTED_CHARTS_DASHBOARD =
'/superset/dashboard/supported_charts_dash/';
export const TABBED_DASHBOARD = '/superset/dashboard/tabbed_dash/';
export const WORLD_HEALTH_DASHBOARD = '/dashboard/world_health/';
export const SAMPLE_DASHBOARD_1 = '/dashboard/1-sample-dashboard/';
export const SUPPORTED_CHARTS_DASHBOARD = '/dashboard/supported_charts_dash/';
export const TABBED_DASHBOARD = '/dashboard/tabbed_dash/';
export const DATABASE_LIST = '/databaseview/list';
+4 -1
View File
@@ -72,7 +72,10 @@ module.exports = {
],
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
transformIgnorePatterns: [
'node_modules/(?!@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact)',
// @ant-design/colors and @ant-design/fast-color are allowed through because
// @ant-design/icons >= 6.3 deep-imports the ESM build of @ant-design/colors
// from its CJS output, so babel-jest must transform those files.
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact)',
],
preset: 'ts-jest',
transform: {
+1191 -3456
View File
File diff suppressed because it is too large Load Diff
+26 -24
View File
@@ -174,17 +174,17 @@
"dayjs": "^1.11.21",
"dom-to-image-more": "^3.10.0",
"dom-to-pdf": "^0.3.2",
"echarts": "^5.6.0",
"echarts": "^6.1.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.3.5",
"fuse.js": "^7.4.2",
"geolib": "^3.3.14",
"geostyler": "^18.6.0",
"geostyler-data": "^1.1.0",
"geostyler-openlayers-parser": "^5.7.0",
"geostyler-openlayers-parser": "^5.7.1",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.7.0",
"google-auth-library": "^10.9.0",
"immer": "^11.1.8",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -192,18 +192,19 @@
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^4.0.0",
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
"mapbox-gl": "^3.25.0",
"markdown-to-jsx": "^9.8.2",
"match-sorter": "^8.3.0",
"memoize-one": "^6.0.0",
"mousetrap": "^1.6.5",
"mustache": "^4.2.0",
"nanoid": "^5.1.15",
"nanoid": "^5.1.16",
"ol": "^10.9.0",
"query-string": "9.4.0",
"query-string": "9.4.1",
"re-resizable": "^6.11.2",
"react": "^18.3.0",
"react-arborist": "^3.10.5",
"react-arborist": "^3.12.0",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
@@ -239,8 +240,7 @@
"use-query-params": "^2.2.2",
"uuid": "^14.0.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"yargs": "^18.0.0",
"lodash-es": "^4.17.21"
"yargs": "^18.0.0"
},
"devDependencies": {
"@babel/cli": "^7.29.7",
@@ -263,11 +263,11 @@
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.15",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.61.0",
"@playwright/test": "^1.61.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.5",
"@storybook/addon-links": "10.4.4",
"@storybook/react-webpack5": "10.4.4",
"@storybook/addon-docs": "10.4.6",
"@storybook/addon-links": "10.4.6",
"@storybook/react-webpack5": "10.4.6",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.43",
@@ -283,8 +283,9 @@
"@types/jquery": "^4.0.1",
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
"@types/node": "^26.0.0",
"@types/node": "^26.0.1",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react-loadable": "^5.5.11",
@@ -297,20 +298,19 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.61.1",
"@typescript-eslint/parser": "^8.61.0",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.1",
"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.10.38",
"baseline-browser-mapping": "^2.10.40",
"cheerio": "1.2.0",
"concurrently": "^10.0.3",
"copy-webpack-plugin": "^14.0.0",
"cross-env": "^10.1.0",
"css-loader": "^7.1.4",
"css-minimizer-webpack-plugin": "^8.0.0",
"eslint": "^10.5.0",
"eslint": "^10.6.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^4.4.5",
@@ -342,10 +342,11 @@
"lerna": "^9.0.4",
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.6.1",
"open-cli": "^9.0.0",
"oxlint": "^1.71.0",
"po2json": "^0.4.5",
"prettier": "3.8.4",
"prettier": "3.9.3",
"prettier-plugin-packagejson": "^3.0.2",
"process": "^0.11.10",
"react-dnd-test-backend": "^16.0.1",
@@ -358,7 +359,6 @@
"storybook": "10.4.6",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.1",
"ts-jest": "^29.4.11",
"tscw-config": "^1.1.2",
"tsx": "^4.22.4",
@@ -366,14 +366,13 @@
"unzipper": "^0.12.5",
"vm-browserify": "^1.1.2",
"wait-on": "^9.0.10",
"webpack": "^5.107.2",
"webpack": "^5.108.3",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^7.0.3",
"webpack-dev-server": "^5.2.5",
"webpack-manifest-plugin": "^6.0.1",
"webpack-sources": "^3.5.0",
"webpack-visualizer-plugin2": "^2.0.0",
"@types/lodash-es": "^4.17.12"
"webpack-visualizer-plugin2": "^2.0.0"
},
"peerDependencies": {
"ace-builds": "^1.41.0",
@@ -394,7 +393,6 @@
"http-proxy-middleware": "^2.0.10",
"tar": "^7.5.16",
"puppeteer": "^22.4.1",
"remark-gfm": "^3.0.1",
"underscore": "^1.13.7",
"jspdf": "^4.2.0",
"nwsapi": "^2.2.13",
@@ -413,6 +411,7 @@
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"fast-xml-parser": "^5.8.0",
"fast-uri": "^3.1.3",
"jest-mock": "^30.4.0",
"jest-runtime": "^30.4.0",
"@jest/globals": "^30.4.0",
@@ -428,6 +427,9 @@
},
"eslint-plugin-jest-dom": {
"eslint": "$eslint"
},
"lerna": {
"js-yaml": "^4.3.0"
}
},
"readme": "ERROR: No README data found!",
@@ -122,6 +122,6 @@
"access": "public"
},
"dependencies": {
"lodash-es": "^4.17.21"
"lodash-es": "^4.18.1"
}
}
@@ -237,6 +237,37 @@ export interface SupersetSpecificTokens {
* Fallback: transparent
*/
buttonSecondaryActiveBorderColor?: string;
// Component flexibility tokens (sizing, radius, behavior)
selectOptionActiveOutline?: boolean;
labelBorderRadius?: number;
buttonControlHeight?: number;
buttonControlHeightSM?: number;
buttonControlHeightXS?: number;
buttonPaddingInline?: number;
buttonPaddingInlineSM?: number;
buttonFontSize?: number;
buttonBorderRadius?: number;
buttonStyleMap?: Record<
string,
{ type?: string; variant?: string; color?: string }
>;
// Dashboard tile tokens (opt-in, fallbacks: colorBgContainer bg, no border, borderRadius, hairline box-shadow)
dashboardTileBg?: string;
dashboardTileBorder?: string;
dashboardTileBorderRadius?: number;
dashboardTileBoxShadow?: string;
/**
* Results grid customization tokens.
* Control the appearance of AG Grid-backed result tables (e.g. SQL Lab).
*/
resultsGridRowHeight?: number;
resultsGridHeaderFontWeight?: number;
resultsGridHeaderFontSize?: number;
resultsGridBorderRadius?: number;
resultsGridNoStriping?: boolean;
}
/**
@@ -173,3 +173,82 @@ test('resetTranslation does nothing when not yet configured', () => {
consoleSpy.mockRestore();
});
});
// --- autoConfigureFromWindow ----------------------------------------------
// These cover the bootstrap-injection path used to dodge the
// module-level `const X = t(...)` race across code-split chunks
// (upstream issue #35330).
test('t() self-configures from window.__SUPERSET_LANGUAGE_PACK__ on first call', () => {
jest.isolateModules(() => {
window.__SUPERSET_LANGUAGE_PACK__ = {
domain: 'superset',
locale_data: {
superset: {
'': {
domain: 'superset',
lang: 'fr',
plural_forms: 'nplurals=2; plural=(n > 1);',
},
hello: ['bonjour'],
},
},
};
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { t } = require('./TranslatorSingleton');
expect(t('hello')).toBe('bonjour');
// No "should call configure" warning because we self-configured first.
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
delete window.__SUPERSET_LANGUAGE_PACK__;
});
});
test('t() falls back to msgid when window has no language pack', () => {
jest.isolateModules(() => {
delete window.__SUPERSET_LANGUAGE_PACK__;
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { t } = require('./TranslatorSingleton');
expect(t('hello')).toBe('hello');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringMatching(/was called before configure\(\)/),
);
consoleSpy.mockRestore();
});
});
test('explicit configure() takes precedence over window pack', () => {
jest.isolateModules(() => {
window.__SUPERSET_LANGUAGE_PACK__ = {
domain: 'superset',
locale_data: {
superset: {
'': {
domain: 'superset',
lang: 'fr',
plural_forms: 'nplurals=2; plural=(n > 1);',
},
hello: ['bonjour'],
},
},
};
const { configure, t } = require('./TranslatorSingleton');
configure({
languagePack: {
domain: 'superset',
locale_data: {
superset: {
'': {
domain: 'superset',
lang: 'es',
plural_forms: 'nplurals=2; plural=(n != 1);',
},
hello: ['hola'],
},
},
},
});
expect(t('hello')).toBe('hola');
delete window.__SUPERSET_LANGUAGE_PACK__;
});
});
@@ -36,7 +36,31 @@ function configure(config?: TranslatorConfig) {
return singleton;
}
// When webpack splits @apache-superset/core across chunks, each
// chunk-local copy of this module has its own `singleton` and
// `isConfigured` state. The first `t()` call in a late chunk hits a
// fresh, unconfigured Translator and returns the English msgid. To
// make translations survive chunk duplication, the HTML template
// stashes the language pack on window and we self-configure from it
// on first access. See upstream issue #35330.
declare global {
interface Window {
__SUPERSET_LANGUAGE_PACK__?: TranslatorConfig['languagePack'];
}
}
function autoConfigureFromWindow() {
if (isConfigured) return;
if (typeof window === 'undefined') return;
const pack = window.__SUPERSET_LANGUAGE_PACK__;
if (pack) {
configure({ languagePack: pack });
}
}
function getInstance() {
autoConfigureFromWindow();
if (typeof singleton === 'undefined') {
singleton = new Translator();
}
@@ -85,11 +109,16 @@ function addLocaleData(data: LocaleData) {
}
function t(input: string, ...args: unknown[]) {
// Self-configure from the bootstrap-injected window pack before deciding
// whether to warn, so a chunk-local copy that hasn't seen configure() yet
// doesn't warn (or fall back to English) when the pack is available.
autoConfigureFromWindow();
if (!isConfigured) warnPreConfigure('t', input);
return getInstance().translate(input, ...args);
}
function tn(key: string, ...args: unknown[]) {
autoConfigureFromWindow();
if (!isConfigured) warnPreConfigure('tn', key);
return getInstance().translateWithNumber(key, ...args);
}
@@ -28,7 +28,7 @@
"@types/react": "*",
"lodash": "^4.18.1",
"tinycolor2": "*",
"lodash-es": "^4.17.21"
"lodash-es": "^4.18.1"
},
"peerDependencies": {
"@ant-design/icons": "^5.6.1",
@@ -238,11 +238,16 @@ export const xAxisForceCategoricalControl = {
return state?.form_data?.x_axis_sort !== undefined || control.value;
},
renderTrigger: true,
// Expose the toggle for numeric and temporal x-axes. Temporal columns
// default to a continuous time scale, where ECharts places ticks at "nice"
// intervals that don't align with the actual buckets (e.g. weekly grain
// markers landing between month ticks). Treating the axis as categorical
// lets each bucket map to a discrete, tick-aligned category.
visibility: ({ controls }: { controls: ControlStateMapping }) =>
checkColumnType(
getColumnLabel(controls?.x_axis?.value as QueryFormColumn),
controls?.datasource?.datasource,
[GenericDataType.Numeric],
[GenericDataType.Numeric, GenericDataType.Temporal],
),
shouldMapStateToProps: () => true,
},
@@ -330,8 +330,7 @@ export interface SelectControlConfig<
optionRenderer?: (option: O) => ReactNode;
valueRenderer?: (option: O) => ReactNode;
filterOption?:
| ((option: FilterOption<O>, rawInput: string) => boolean)
| null;
((option: FilterOption<O>, rawInput: string) => boolean) | null;
}
export type SharedControlConfig<
@@ -396,9 +395,7 @@ export const isCustomControlItem = (obj: unknown): obj is CustomControlItem =>
export type ExpandedControlItem = CustomControlItem | ReactElement | null;
export type ControlSetItem =
| SharedControlAlias
| OverrideSharedControlItem
| ExpandedControlItem;
SharedControlAlias | OverrideSharedControlItem | ExpandedControlItem;
export type ControlSetRow = ControlSetItem[];
@@ -702,10 +699,7 @@ export interface DataColumnMeta {
originalLabel?: string;
dataType: GenericDataType;
formatter?:
| TimeFormatter
| NumberFormatter
| CustomFormatter
| CurrencyFormatter;
TimeFormatter | NumberFormatter | CustomFormatter | CurrencyFormatter;
isMetric?: boolean;
isPercentMetric?: boolean;
isNumeric?: boolean;
@@ -713,4 +707,5 @@ export interface DataColumnMeta {
isChildColumn?: boolean;
description?: string;
currencyCodeColumn?: string;
isFilterable?: boolean;
}
@@ -20,7 +20,11 @@
import { GenericDataType } from '@apache-superset/core/common';
import { xAxisForceCategoricalControl } from '../../src/shared-controls/customControls';
import { checkColumnType } from '../../src/utils/checkColumnType';
import type { ControlState } from '@superset-ui/chart-controls';
import type {
ControlPanelState,
ControlState,
ControlStateMapping,
} from '@superset-ui/chart-controls';
jest.mock('../../src/utils/checkColumnType');
jest.mock('@superset-ui/core', () => ({
@@ -39,12 +43,12 @@ test('xAxisForceCategoricalControl should not treat temporal columns as categori
controls: {
x_axis: { value: 'date_column' },
datasource: { datasource: {} },
},
};
} as unknown as ControlStateMapping,
} as unknown as ControlPanelState;
const result = xAxisForceCategoricalControl.config.initialValue!(
control,
state as any,
state,
);
// Verify: should return control value (false) for non-numeric columns
@@ -55,3 +59,27 @@ test('xAxisForceCategoricalControl should not treat temporal columns as categori
mockCheckColumnType.mockClear();
});
test('xAxisForceCategoricalControl is visible for numeric and temporal x-axes', () => {
const mockCheckColumnType = jest.mocked(checkColumnType);
mockCheckColumnType.mockReturnValue(true);
const controls = {
x_axis: { value: 'date_column' },
datasource: { datasource: {} },
} as unknown as ControlStateMapping;
const visible = xAxisForceCategoricalControl.config.visibility!({
controls,
});
expect(visible).toBe(true);
// Temporal columns must be included so the toggle is exposed for time-grain
// charts (e.g. weekly grain), where the time scale misaligns ticks/markers.
expect(mockCheckColumnType).toHaveBeenCalledWith('date_column', {}, [
GenericDataType.Numeric,
GenericDataType.Temporal,
]);
mockCheckColumnType.mockClear();
});
@@ -24,7 +24,7 @@
"lib"
],
"dependencies": {
"@ant-design/icons": "^6.2.5",
"@ant-design/icons": "^6.3.2",
"@apache-superset/core": "*",
"@babel/runtime": "^7.29.7",
"@braintree/sanitize-url": "^7.1.2",
@@ -53,21 +53,21 @@
"re-resizable": "^6.11.2",
"react-ace": "^14.0.1",
"react-draggable": "^4.7.0",
"react-error-boundary": "6.0.0",
"react-error-boundary": "^6.1.2",
"react-js-cron": "^5.2.0",
"react-markdown": "^8.0.7",
"react-markdown": "^9.0.7",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^16.1.1",
"react-ultimate-pagination": "^1.3.2",
"regenerator-runtime": "^0.14.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^3.0.1",
"remark-gfm": "^4.0.1",
"reselect": "^5.2.0",
"rison": "^0.1.1",
"seedrandom": "^3.0.5",
"xss": "^1.0.15",
"lodash-es": "^4.17.21"
"lodash-es": "^4.18.1"
},
"devDependencies": {
"@emotion/styled": "^11.14.1",
@@ -78,7 +78,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.1",
"@types/lodash": "^4.17.24",
"@types/node": "^26.0.0",
"@types/node": "^26.0.1",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -109,7 +109,7 @@ export default class ChartClient {
(await buildQueryRegistry.get(visType)) ?? (() => formData);
const requestConfig: RequestConfig = useLegacyApi
? {
endpoint: '/superset/explore_json/',
endpoint: '/explore_json/',
postPayload: {
form_data: buildQuery(formData),
},
@@ -139,7 +139,7 @@ export default class ChartClient {
): Promise<Datasource> {
return this.client
.get({
endpoint: `/superset/fetch_datasource_metadata?datasourceKey=${datasourceKey}`,
endpoint: `/fetch_datasource_metadata?datasourceKey=${datasourceKey}`,
...options,
} as RequestConfig)
.then(response => response.json as Datasource);
@@ -28,6 +28,7 @@ import {
RequestConfig,
getClientErrorObject,
} from '../..';
import type { HandlerFunction } from '../types/Base';
import { Loading } from '../../components/Loading';
import ChartClient from '../clients/ChartClient';
import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton';
@@ -262,9 +263,7 @@ export default function StatefulChart(props: StatefulChartProps) {
if (!useLegacyApi && !queryContext.queries) {
queryContext = { queries: [queryContext] };
}
const endpoint = useLegacyApi
? '/superset/explore_json/'
: '/api/v1/chart/data';
const endpoint = useLegacyApi ? '/explore_json/' : '/api/v1/chart/data';
const requestConfig: RequestConfig = {
endpoint,
@@ -482,7 +481,7 @@ export default function StatefulChart(props: StatefulChartProps) {
enableNoResults={enableNoResults}
noResults={NoDataComponent && <NoDataComponent />}
onRenderSuccess={onRenderSuccess}
onRenderFailure={onRenderFailure}
onRenderFailure={onRenderFailure as HandlerFunction | undefined}
hooks={hooks}
/>
);
@@ -29,13 +29,7 @@ import {
import type { AsyncAceEditorOptions } from './types';
type EditorType =
| 'sql'
| 'full-sql'
| 'markdown'
| 'text-area'
| 'css'
| 'json'
| 'config';
'sql' | 'full-sql' | 'markdown' | 'text-area' | 'css' | 'json' | 'config';
const editorTypes: EditorType[] = [
'sql',
@@ -70,6 +70,199 @@ test('SQLEditor uses fontFamilyCode from theme', async () => {
expect(fontFamily).toMatch(/mono|courier|consolas/i);
});
test('re-measures Ace font metrics on load and preserves a consumer onLoad (#41664)', async () => {
// Ace caches glyph width at construction; if the editor font settles later,
// the caret drifts. The editor forces a re-measure on load and again once
// `document.fonts.ready` resolves. Mock `document.fonts` so the re-measure
// path is deterministic regardless of the jsdom FontFaceSet implementation.
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
const fontsReady = Promise.resolve();
Object.defineProperty(document, 'fonts', {
configurable: true,
value: { ready: fontsReady },
});
const ref = createRef<AceEditor>();
let updateFontSizeSpy: jest.SpyInstance | undefined;
// The spy is installed from inside the consumer onLoad, so it captures the
// asynchronous (post-fonts-ready) re-measure that runs after this callback.
const consumerOnLoad = jest.fn((editor: AceEditor['editor']) => {
// Cast to a minimal shape so `jest.spyOn` resolves cleanly; it is the
// same renderer instance the component re-measures, so the spy still
// observes the production calls.
const renderer = editor.renderer as unknown as {
updateFontSize: () => void;
};
updateFontSizeSpy = jest.spyOn(renderer, 'updateFontSize');
});
try {
const { container } = render(
<SQLEditor
ref={ref as React.Ref<never>}
onLoad={consumerOnLoad as never}
/>,
);
await waitFor(() => {
expect(container.querySelector(selector)).toBeInTheDocument();
});
// The wrapper must call through to the consumer's onLoad with the editor.
expect(consumerOnLoad).toHaveBeenCalledTimes(1);
expect(consumerOnLoad).toHaveBeenCalledWith(ref.current?.editor);
// Once fonts settle, the editor re-measures (Ace itself resizes and
// re-renders when the measured character size changed) so the caret
// realigns.
await fontsReady;
await waitFor(() => {
expect(updateFontSizeSpy).toHaveBeenCalled();
});
} finally {
updateFontSizeSpy?.mockRestore();
if (originalFonts) {
Object.defineProperty(document, 'fonts', originalFonts);
} else {
delete (document as { fonts?: unknown }).fonts;
}
}
});
test('does not crash when the Font Loading API is unavailable (#41697)', async () => {
// jsdom has no `document.fonts` by default; some embedded webviews don't
// either. The load-time re-measure must still run and the editor must mount.
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
delete (document as { fonts?: unknown }).fonts;
const consumerOnLoad = jest.fn();
try {
const { container } = render(
<SQLEditor onLoad={consumerOnLoad as never} />,
);
await waitFor(() => {
expect(container.querySelector(selector)).toBeInTheDocument();
});
expect(consumerOnLoad).toHaveBeenCalledTimes(1);
} finally {
if (originalFonts) {
Object.defineProperty(document, 'fonts', originalFonts);
}
}
});
test('explicitly loads the editor font and re-measures when it resolves (#41664)', async () => {
// `fonts.ready` can settle BEFORE a lazily-referenced editor font even
// starts loading, so the fix must request the font itself via
// `fonts.load()` and re-measure when that specific load resolves.
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
let resolveFontLoad: () => void = () => {};
const fontLoad = new Promise<void>(resolve => {
resolveFontLoad = resolve;
});
const load = jest.fn((_font: string) => fontLoad);
Object.defineProperty(document, 'fonts', {
configurable: true,
// `ready` already settled: the pre-fix code would never re-measure.
value: {
ready: Promise.resolve(),
load,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
},
});
let updateFontSizeSpy: jest.SpyInstance | undefined;
const consumerOnLoad = jest.fn((editor: AceEditor['editor']) => {
const renderer = editor.renderer as unknown as {
updateFontSize: () => void;
};
updateFontSizeSpy = jest.spyOn(renderer, 'updateFontSize');
});
try {
const { container } = render(
<SQLEditor onLoad={consumerOnLoad as never} />,
);
await waitFor(() => {
expect(container.querySelector(selector)).toBeInTheDocument();
});
// The editor font itself is requested (family from the theme's code font).
expect(load).toHaveBeenCalledTimes(1);
expect(load.mock.calls[0][0]).toMatch(/^12px /);
const callsBefore = updateFontSizeSpy?.mock.calls.length ?? 0;
resolveFontLoad();
await waitFor(() => {
expect(updateFontSizeSpy!.mock.calls.length).toBeGreaterThan(callsBefore);
});
} finally {
updateFontSizeSpy?.mockRestore();
if (originalFonts) {
Object.defineProperty(document, 'fonts', originalFonts);
} else {
delete (document as { fonts?: unknown }).fonts;
}
}
});
test('re-measures on later loadingdone events and unsubscribes on unmount (#41664)', async () => {
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
const listeners: Record<string, (() => void)[]> = {};
const addEventListener = jest.fn((event: string, handler: () => void) => {
(listeners[event] ??= []).push(handler);
});
const removeEventListener = jest.fn((event: string, handler: () => void) => {
listeners[event] = (listeners[event] ?? []).filter(h => h !== handler);
});
Object.defineProperty(document, 'fonts', {
configurable: true,
value: {
ready: Promise.resolve(),
load: jest.fn(() => Promise.resolve()),
addEventListener,
removeEventListener,
},
});
let updateFontSizeSpy: jest.SpyInstance | undefined;
const consumerOnLoad = jest.fn((editor: AceEditor['editor']) => {
const renderer = editor.renderer as unknown as {
updateFontSize: () => void;
};
updateFontSizeSpy = jest.spyOn(renderer, 'updateFontSize');
});
try {
const { container, unmount } = render(
<SQLEditor onLoad={consumerOnLoad as never} />,
);
await waitFor(() => {
expect(container.querySelector(selector)).toBeInTheDocument();
});
await waitFor(() => {
expect(listeners.loadingdone?.length ?? 0).toBeGreaterThan(0);
});
// A font finishing later (e.g. a user-config font) re-measures again.
const callsBefore = updateFontSizeSpy?.mock.calls.length ?? 0;
listeners.loadingdone.forEach(handler => handler());
expect(updateFontSizeSpy!.mock.calls.length).toBeGreaterThan(callsBefore);
// Unmount removes the listener so destroyed editors are not re-measured.
unmount();
expect(listeners.loadingdone?.length ?? 0).toBe(0);
} finally {
updateFontSizeSpy?.mockRestore();
if (originalFonts) {
Object.defineProperty(document, 'fonts', originalFonts);
} else {
delete (document as { fonts?: unknown }).fonts;
}
}
});
test('renders FullSQLEditor', async () => {
const { container } = render(<FullSQLEditor />);
@@ -0,0 +1,219 @@
/**
* 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 ace from 'ace-builds/src-noconflict/ace';
import {
emojiWidth,
isBmpEmojiPresentation,
patchAceEmojiWidths,
} from './emojiWidthPatch';
const { EditSession } = ace.require('ace/edit_session');
const { Text: TextLayer } = ace.require('ace/layer/text');
beforeAll(() => {
patchAceEmojiWidths(EditSession, TextLayer);
});
test('classifies BMP emoji-presentation codepoints and nothing else', () => {
expect(isBmpEmojiPresentation('✨'.charCodeAt(0))).toBe(true); // U+2728
expect(isBmpEmojiPresentation('⭐'.charCodeAt(0))).toBe(true); // U+2B50
expect(isBmpEmojiPresentation('❤'.charCodeAt(0))).toBe(false); // text-default
expect(isBmpEmojiPresentation('A'.charCodeAt(0))).toBe(false);
expect(isBmpEmojiPresentation('中'.charCodeAt(0))).toBe(false); // CJK, ace's own tables
expect(isBmpEmojiPresentation(0xd83d)).toBe(false); // lone surrogate
});
test('emojiWidth: VS16 tops its base up to 2 columns total', () => {
const VS16 = 0xfe0f;
expect(emojiWidth('✨'.charCodeAt(0), 0)).toBe(2);
// ❤️ = U+2764 (1 by default rules) + VS16 (1) = 2 total
expect(emojiWidth('❤'.charCodeAt(0), 0)).toBe(null);
expect(emojiWidth(VS16, '❤'.charCodeAt(0))).toBe(1);
// Redundant ✨️ = ✨ (2) + VS16 (0) = 2 total
expect(emojiWidth(VS16, '✨'.charCodeAt(0))).toBe(0);
expect(emojiWidth('A'.charCodeAt(0), 0)).toBe(null);
});
test('caret math counts emoji as two columns (issue #41664 repro line)', () => {
const session = new EditSession('### ✨Header 3');
// Before "H" (doc column 5): "### " = 4 + ✨ = 2 → screen column 6.
expect(session.documentToScreenColumn(0, 5)).toBe(6);
// End of line: 4 + 2 + "Header 3".length (8) = 14.
expect(session.documentToScreenColumn(0, 13)).toBe(14);
});
test('astral emoji keep their pre-existing two-column accounting', () => {
const session = new EditSession('💡X');
// 💡 is two code units (doc columns 0-1) and two screen columns.
expect(session.documentToScreenColumn(0, 2)).toBe(2);
});
test('$getStringScreenWidth counts emoji, VS16 pairs, and plain text', () => {
const session = new EditSession('');
expect(session.$getStringScreenWidth('✨')[0]).toBe(2);
expect(session.$getStringScreenWidth('❤️')[0]).toBe(2);
expect(session.$getStringScreenWidth('✨️')[0]).toBe(2);
expect(session.$getStringScreenWidth('abc')[0]).toBe(3);
expect(session.$getStringScreenWidth('中')[0]).toBe(2); // ace's own CJK path
});
test('renderer draws emoji in a forced 2×charWidth box like CJK', () => {
const container = document.createElement('div');
const layer = new TextLayer(container);
layer.config = { characterWidth: 10 };
const parent = document.createElement('div');
const screenColumn = layer.$renderToken(
parent,
0,
{ type: 'text', value: '# ✨Header' },
'# ✨Header',
);
const box = parent.querySelector<HTMLElement>('.ace_cjk');
expect(box).not.toBeNull();
expect(box?.textContent).toBe('✨');
expect(box?.style.width).toBe('20px');
// "# " (2) + ✨ (2) + "Header" (6) = 10 columns.
expect(screenColumn).toBe(10);
// Text around the emoji is preserved in order.
expect(parent.textContent).toBe('# ✨Header');
});
test('renderer handles a leading emoji (empty split part) cleanly', () => {
const container = document.createElement('div');
const layer = new TextLayer(container);
layer.config = { characterWidth: 10 };
const parent = document.createElement('div');
const screenColumn = layer.$renderToken(
parent,
0,
{ type: 'text', value: '✨x' },
'✨x',
);
expect(parent.querySelector('.ace_cjk')?.textContent).toBe('✨');
expect(parent.textContent).toBe('✨x');
expect(screenColumn).toBe(3);
});
test('renderer leaves emoji-free tokens entirely to the original path', () => {
const container = document.createElement('div');
const layer = new TextLayer(container);
layer.config = { characterWidth: 10 };
const parent = document.createElement('div');
const screenColumn = layer.$renderToken(
parent,
0,
{ type: 'text', value: 'SELECT 1' },
'SELECT 1',
);
expect(parent.querySelector('.ace_cjk')).toBeNull();
expect(parent.textContent).toBe('SELECT 1');
expect(screenColumn).toBe(8);
});
test('emoji boxes carry the token-class styling of their token', () => {
const container = document.createElement('div');
const layer = new TextLayer(container);
layer.config = { characterWidth: 10 };
const parent = document.createElement('div');
layer.$renderToken(
parent,
0,
{ type: 'constant.language', value: '✨' },
'✨',
);
const box = parent.querySelector<HTMLElement>('.ace_cjk');
expect(box?.className).toBe('ace_cjk ace_constant ace_language');
});
test('emoji boxes in plain text tokens carry no extra classes', () => {
const container = document.createElement('div');
const layer = new TextLayer(container);
layer.config = { characterWidth: 10 };
const parent = document.createElement('div');
layer.$renderToken(parent, 0, { type: 'text', value: '✨' }, '✨');
const box = parent.querySelector<HTMLElement>('.ace_cjk');
expect(box?.className).toBe('ace_cjk');
});
test('patch is idempotent', () => {
const before = EditSession.prototype.$getStringScreenWidth;
patchAceEmojiWidths(EditSession, TextLayer);
expect(EditSession.prototype.$getStringScreenWidth).toBe(before);
});
// --- copied-internals parity ------------------------------------------------
// The two overridden EditSession methods are verbatim copies of the
// ace-builds internals plus the emoji branch; these pin the non-emoji
// behavior so a future ace upgrade that drifts is caught here.
test('$getStringScreenWidth keeps tab, early-return, and break semantics', () => {
const session = new EditSession('');
// Tab expands to the next tab stop (default tab size 4).
expect(session.$getStringScreenWidth('\ta')[0]).toBe(5);
// maxScreenColumn === 0 short-circuits.
expect(session.$getStringScreenWidth('abc', 0)).toEqual([0, 0]);
// Stops consuming once past maxScreenColumn.
const [screen, column] = session.$getStringScreenWidth('abcdef', 3);
expect(screen).toBe(4);
expect(column).toBe(3);
});
test('$getDisplayTokens keeps tab, space, punctuation, and CJK tokens', () => {
const session = new EditSession('');
const CHAR = 1;
const CHAR_EXT = 2;
const PUNCTUATION = 9;
const SPACE = 10;
const TAB = 11;
const TAB_SPACE = 12;
// Tab at offset 0 with default tab size 4: TAB + 3 TAB_SPACEs.
expect(session.$getDisplayTokens('\t', 0)).toEqual([
TAB,
TAB_SPACE,
TAB_SPACE,
TAB_SPACE,
]);
expect(session.$getDisplayTokens(' a', 0)).toEqual([SPACE, CHAR]);
expect(session.$getDisplayTokens('*:', 0)).toEqual([
PUNCTUATION,
PUNCTUATION,
]);
expect(session.$getDisplayTokens('中', 0)).toEqual([CHAR, CHAR_EXT]);
expect(session.$getDisplayTokens('✨', 0)).toEqual([CHAR, CHAR_EXT]);
// VS16 keeps one entry per code unit so wrap offsets stay aligned.
expect(session.$getDisplayTokens('✨️', 0)).toEqual([
CHAR,
CHAR_EXT,
CHAR_EXT,
]);
expect(session.$getDisplayTokens('❤️', 0)).toEqual([CHAR, CHAR_EXT]);
// Second pass hits the memoized emoji-base classification.
expect(session.$getDisplayTokens('❤️', 0)).toEqual([CHAR, CHAR_EXT]);
});
@@ -0,0 +1,283 @@
/**
* 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.
*/
/**
* Ace positions the caret on a monospace grid (`column × charWidth`) and
* renders anything it classifies as "full width" inside a forced
* `2 × charWidth` inline-block (`.ace_cjk`), so the grid model and the pixels
* agree. Its width tables only cover East-Asian ranges, though:
*
* - Astral emoji (💡, surrogate pairs) happen to work: the renderer's
* surrogate-pair branch forces the 2-cell box and the column counter
* counts the two code units.
* - BMP emoji with default emoji presentation ( U+2728, U+2B50, ) fall
* through BOTH nets: they count as 1 column but render ~1.6 cells wide, so
* the caret drifts on any line containing one (issue #41664, upstream
* ajaxorg/ace#3404).
* - VS16 sequences ( = U+2764 U+FE0F) render as one color glyph while the
* two code units count as two ordinary columns, without the forced box.
*
* The patch extends the exact mechanism Ace already uses count 2 columns
* and force the 2-cell box to emoji-presentation codepoints and VS16
* sequences, by overriding the two `EditSession` width methods (verbatim
* copies of the ace-builds internals plus one branch; the originals call a
* module-private `isFullWidth`, so they cannot be extended in place) and
* wrapping the text layer's `$renderToken` to emit the forced-width span for
* emoji runs before delegating everything else to the original.
*
* Deliberately out of scope: ZWJ sequences (family emoji) and flag pairs
* render as one glyph but count per component; handling them needs grapheme
* segmentation across every Ace layer, and they were equally misaligned
* before this patch.
*/
const VS16 = 0xfe0f;
// Tokens ace renders as bare text nodes with no token-class span; mirrors
// ace/layer/text_util's textTokens set.
const TEXT_TOKENS = new Set(['text', 'rparen', 'lparen']);
// Emoji_Presentation covers exactly the codepoints browsers render as color
// emoji without a variation selector. Lone surrogates never match it (the
// astral path is already consistent), so only BMP emoji change behavior.
const EMOJI_PRESENTATION_RE = /\p{Emoji_Presentation}/u;
// One renderable emoji cluster: an emoji-presentation codepoint with an
// optional (redundant) VS16, or a text-presentation emoji forced to color
// presentation by VS16.
const EMOJI_CLUSTER_SOURCE =
'(?:\\p{Emoji_Presentation}\\uFE0F?|\\p{Emoji}\\uFE0F)';
const EMOJI_RUN_TEST_RE = new RegExp(`${EMOJI_CLUSTER_SOURCE}`, 'u');
const EMOJI_RUN_SPLIT_RE = new RegExp(`(${EMOJI_CLUSTER_SOURCE}+)`, 'gu');
const EMOJI_CLUSTER_RE = new RegExp(EMOJI_CLUSTER_SOURCE, 'gu');
// Memoized per-code-unit classification; $getStringScreenWidth runs per
// character per rendered line, so the regexes must not run every time.
const EMOJI_BASE_RE = /\p{Emoji}/u;
const emojiBaseCache = new Map<number, boolean>();
function isEmojiBase(code: number): boolean {
let result = emojiBaseCache.get(code);
if (result === undefined) {
result = EMOJI_BASE_RE.test(String.fromCharCode(code));
emojiBaseCache.set(code, result);
}
return result;
}
const bmpEmojiCache = new Map<number, boolean>();
export function isBmpEmojiPresentation(code: number): boolean {
if (code < 0x2000 || code > 0xffff || (code >= 0xd800 && code <= 0xdfff)) {
return false;
}
let result = bmpEmojiCache.get(code);
if (result === undefined) {
result = EMOJI_PRESENTATION_RE.test(String.fromCharCode(code));
bmpEmojiCache.set(code, result);
}
return result;
}
/**
* Screen-column contribution of one code unit under the emoji rules, or null
* when Ace's default handling should apply. Emoji-presentation chars are 2
* columns; VS16 tops its base up to 2 in total (so = 1 + 1 and a
* redundant = 2 + 0).
*/
export function emojiWidth(code: number, prevCode: number): number | null {
if (isBmpEmojiPresentation(code)) {
return 2;
}
if (code === VS16) {
return isBmpEmojiPresentation(prevCode) ? 0 : 1;
}
return null;
}
interface PatchableEditSessionClass {
prototype: {
isFullWidth: (c: number) => boolean;
getScreenTabSize: (screenColumn: number) => number;
$getStringScreenWidth: (
str: string,
maxScreenColumn?: number,
screenColumn?: number,
) => [number, number];
$getDisplayTokens: (str: string, offset: number) => number[];
};
}
interface TextLayerInstance {
config: { characterWidth: number };
dom: { createElement: (tag: string) => HTMLElement };
}
type RenderToken = (
this: TextLayerInstance,
parent: HTMLElement,
screenColumn: number,
token: { type: string; value: string },
value: string,
) => number;
interface PatchableTextLayerClass {
prototype: { $renderToken: RenderToken };
}
let patched = false;
export function patchAceEmojiWidths(
EditSession: PatchableEditSessionClass,
TextLayer: PatchableTextLayerClass,
): void {
if (patched) {
return;
}
patched = true;
const sessionProto = EditSession.prototype;
const { isFullWidth } = sessionProto;
// Mirrors EditSession.prototype.$getStringScreenWidth from ace-builds
// (src-noconflict/ace.js), adding the emojiWidth branch.
sessionProto.$getStringScreenWidth = function $getStringScreenWidth(
this: PatchableEditSessionClass['prototype'],
str: string,
maxScreenColumn?: number,
screenColumn?: number,
): [number, number] {
if (maxScreenColumn === 0) {
return [0, 0];
}
const max = maxScreenColumn ?? Infinity;
let screen = screenColumn || 0;
let column;
for (column = 0; column < str.length; column += 1) {
const c = str.charCodeAt(column);
const emoji = emojiWidth(c, column > 0 ? str.charCodeAt(column - 1) : 0);
if (c === 9) {
screen += this.getScreenTabSize(screen);
} else if (emoji !== null) {
screen += emoji;
} else if (c >= 0x1100 && isFullWidth(c)) {
screen += 2;
} else {
screen += 1;
}
if (screen > max) {
break;
}
}
return [screen, column];
};
// Display-token codes from ace's edit_session module.
const CHAR = 1;
const CHAR_EXT = 2;
const PUNCTUATION = 9;
const SPACE = 10;
const TAB = 11;
const TAB_SPACE = 12;
// Mirrors EditSession.prototype.$getDisplayTokens, adding the emoji branch.
// One entry per code unit is preserved (VS16 pushes a lone CHAR_EXT) so the
// wrap-split offset math stays aligned with document columns.
sessionProto.$getDisplayTokens = function $getDisplayTokens(
this: PatchableEditSessionClass['prototype'],
str: string,
offset: number,
): number[] {
const arr: number[] = [];
let tabSize: number;
for (let i = 0; i < str.length; i += 1) {
const c = str.charCodeAt(i);
const emoji = emojiWidth(c, i > 0 ? str.charCodeAt(i - 1) : 0);
if (c === 9) {
tabSize = this.getScreenTabSize(arr.length + offset);
arr.push(TAB);
for (let n = 1; n < tabSize; n += 1) {
arr.push(TAB_SPACE);
}
} else if (emoji === 2) {
arr.push(CHAR, CHAR_EXT);
} else if (emoji === 0) {
arr.push(CHAR_EXT);
} else if (c === VS16 && i > 0 && isEmojiBase(str.charCodeAt(i - 1))) {
// VS16 after a text-presentation emoji base (❤️): keep the pair
// atomic for wrap splitting; its width contribution stays 1.
arr.push(CHAR_EXT);
} else if (c === 32) {
arr.push(SPACE);
} else if ((c > 39 && c < 48) || (c > 57 && c < 64)) {
arr.push(PUNCTUATION);
} else if (c >= 0x1100 && isFullWidth(c)) {
arr.push(CHAR, CHAR_EXT);
} else {
arr.push(CHAR);
}
}
return arr;
};
// Pre-split token text on emoji runs: emoji clusters get the same forced
// 2 × charWidth `.ace_cjk` box Ace gives CJK (so rendered geometry matches
// the 2-column model above); everything else delegates to the original.
const origRenderToken = TextLayer.prototype.$renderToken;
TextLayer.prototype.$renderToken = function $renderToken(
this: TextLayerInstance,
parent: HTMLElement,
screenColumn: number,
token: { type: string; value: string },
value: string,
): number {
if (!EMOJI_RUN_TEST_RE.test(value)) {
return origRenderToken.call(this, parent, screenColumn, token, value);
}
// Non-text tokens normally have their whole fragment wrapped in a
// token-class span ("ace_" + dotted type); carry those classes on the
// emoji box itself so syntax styling that isn't glyph color (comment
// italics, invalid-token backgrounds, …) still applies to emoji.
// Mirrors ace's text_util.isTextToken and $renderToken class handling.
const tokenClasses = TEXT_TOKENS.has(token.type)
? ''
: ` ace_${token.type.replace(/\./g, ' ace_')}`;
let column = screenColumn;
value.split(EMOJI_RUN_SPLIT_RE).forEach((part, index) => {
if (!part) {
return;
}
const isEmojiRun = index % 2 === 1;
if (isEmojiRun) {
// Parts at odd indices are the split's capture group, so every one
// is a non-empty sequence of clusters; matchAll never comes up empty.
Array.from(part.matchAll(EMOJI_CLUSTER_RE), m => m[0]).forEach(
cluster => {
const span = this.dom.createElement('span');
span.style.width = `${this.config.characterWidth * 2}px`;
span.className = `ace_cjk${tokenClasses}`;
span.textContent = cluster;
parent.appendChild(span);
column += 2;
},
);
} else {
column = origRenderToken.call(this, parent, column, token, part);
}
});
return column;
};
}
@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef, useEffect, useCallback, ComponentType } from 'react';
import {
forwardRef,
useEffect,
useCallback,
useRef,
ComponentType,
} from 'react';
import type {
Editor as OrigEditor,
@@ -35,6 +41,8 @@ import {
import { useTheme, css, type SupersetTheme } from '@apache-superset/core/theme';
import { Global } from '@emotion/react';
import { patchAceEmojiWidths } from './emojiWidthPatch';
export { getTooltipHTML } from './Tooltip';
export { useJsonValidation } from './useJsonValidation';
export type {
@@ -162,6 +170,14 @@ export function AsyncAceEditor(
config.setModuleUrl('ace/mode/javascript_worker', javascriptWorkerUrl);
config.setModuleUrl('ace/mode/html_worker', htmlWorkerUrl);
// Align caret math and rendered glyph geometry for emoji (issue #41664);
// see emojiWidthPatch for the full story. Applied once, globally, since
// the prototypes are shared by every Ace editor instance.
patchAceEmojiWidths(
acequire('ace/edit_session').EditSession,
acequire('ace/layer/text').Text,
);
await Promise.all(aceModules.map(x => aceModuleLoaders[x]()));
const inferredMode =
@@ -179,6 +195,7 @@ export function AsyncAceEditor(
theme = inferredTheme,
tabSize = defaultTabSize,
defaultValue = '',
onLoad,
...props
},
ref,
@@ -281,6 +298,62 @@ export function AsyncAceEditor(
};
}, [ref]);
// Ace caches the measured glyph width in its internal FontMetrics and
// only re-measures when its hidden measure node's own size changes. If
// the editor font finishes loading after construction, the cached width
// can stop matching the rendered glyphs and the caret drifts further
// from the text the longer the line, the residual misalignment in issue
// #41664 that the font-family CSS from #38928 does not address.
// `updateFontSize` runs Ace's `checkForSizeChanges`, which on a metrics
// change emits `changeCharacterSize`; Ace's renderer reacts with a
// forced resize and full re-render.
//
// Re-measure on every signal that glyph geometry may have changed:
// - immediately, for the already-loaded case;
// - after explicitly loading the editor font via `fonts.load()` —
// `fonts.ready` alone is NOT enough, because it can settle before a
// lazily-referenced editor font even starts loading, in which case
// waiting on it misses the swap entirely;
// - after `fonts.ready`, for everything in flight at mount;
// - on every subsequent `loadingdone` event while mounted, for fonts
// that load even later (removed on unmount via fontCleanupsRef).
const fontCleanupsRef = useRef<(() => void)[]>([]);
useEffect(
() => () => {
fontCleanupsRef.current.forEach(cleanup => cleanup());
fontCleanupsRef.current = [];
},
[],
);
const handleLoad = useCallback(
(editor: Ace.Editor) => {
const remeasure = () => {
try {
editor.renderer.updateFontSize();
} catch {
// The editor was destroyed before a pending font event fired.
}
};
remeasure();
const { fonts } = document;
if (fonts) {
// The size in the shorthand is irrelevant; any value triggers
// the load of the family's faces.
fonts
.load?.(`12px ${editorFontFamily}`)
?.then(remeasure)
?.catch(() => {});
fonts.ready?.then(remeasure)?.catch(() => {});
fonts.addEventListener?.('loadingdone', remeasure);
fontCleanupsRef.current.push(() =>
fonts.removeEventListener?.('loadingdone', remeasure),
);
}
onLoad?.(editor);
},
[onLoad, editorFontFamily],
);
return (
<>
<Global
@@ -316,8 +389,9 @@ export function AsyncAceEditor(
}
/* Adjust selection color */
.ace_editor .ace_selection {
background-color: ${token.colorEditorSelection ??
token.colorPrimaryBgHover} !important;
background-color: ${
token.colorEditorSelection ?? token.colorPrimaryBgHover
} !important;
}
/* Improve active line highlighting */
@@ -468,6 +542,7 @@ export function AsyncAceEditor(
tabSize={tabSize}
defaultValue={defaultValue}
setOptions={{ fontFamily: editorFontFamily }}
onLoad={handleLoad}
{...props}
/>
</>
@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useState, forwardRef, ComponentType } from 'react';
import React, {
useEffect,
useState,
forwardRef,
ComponentType,
ForwardedRef,
} from 'react';
import { Loading } from '../Loading';
import type { PlaceholderProps } from './types';
@@ -93,7 +99,7 @@ export function AsyncEsmComponent<
// @ts-expect-error -- generic forwardRef has PropsWithoutRef incompatibility with FullProps
const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent(
props: FullProps,
ref,
ref: ForwardedRef<ComponentType<FullProps>>,
) {
const [loaded, setLoaded] = useState(component !== undefined);
useEffect(() => {
@@ -125,6 +125,11 @@ InteractiveButton.argTypes = {
options: buttonSizes,
control: { type: 'select' },
},
styleConfig: {
description:
'Optional visual overrides (controlHeight, paddingInline, fontSize, fontWeight, borderRadius, ctaMinWidth, ctaMinHeight, iconGap).',
control: { type: 'object' },
},
target: {
name: TARGETS.label,
control: { type: 'select' },
@@ -186,6 +186,68 @@ test('getSecondaryButtonHoverStyles supports partial token overrides', () => {
expect(hoverStyles['&:active'].backgroundColor).toBe('#99d3df !important');
});
test('styleConfig overrides theme defaults', () => {
const { getByRole } = render(
<Button
buttonStyle="primary"
styleConfig={{
controlHeight: 50,
fontSize: 20,
fontWeight: 900,
paddingInline: 30,
borderRadius: 12,
}}
>
Custom
</Button>,
);
expect(getByRole('button')).toBeInTheDocument();
});
test('styleConfig partial override merges with defaults', () => {
const { getByRole } = render(
<Button buttonStyle="primary" styleConfig={{ controlHeight: 44 }}>
Partial
</Button>,
);
expect(getByRole('button')).toBeInTheDocument();
});
test('xsmall size resolves with theme tokens', () => {
const { getByRole } = render(
<Button buttonSize="xsmall" buttonStyle="primary">
XSmall
</Button>,
);
expect(getByRole('button')).toBeInTheDocument();
expect(getByRole('button')).toHaveClass('superset-button');
});
test('small size resolves with theme tokens', () => {
const { getByRole } = render(
<Button buttonSize="small" buttonStyle="primary">
Small
</Button>,
);
expect(getByRole('button')).toBeInTheDocument();
expect(getByRole('button')).toHaveClass('superset-button');
});
test('primary buttonStyle applies ant-btn-primary class', () => {
const { getByRole } = render(<Button buttonStyle="primary">Primary</Button>);
expect(getByRole('button')).toHaveClass('ant-btn-primary');
});
test('danger buttonStyle applies ant-btn-dangerous class', () => {
const { getByRole } = render(<Button buttonStyle="danger">Danger</Button>);
expect(getByRole('button')).toHaveClass('ant-btn-dangerous');
});
test('link buttonStyle applies ant-btn-link class', () => {
const { getByRole } = render(<Button buttonStyle="link">Link</Button>);
expect(getByRole('button')).toHaveClass('ant-btn-link');
});
test('getSecondaryButtonStyle falls back when tokens are empty strings', () => {
const mockTheme = {
colorPrimary: '#2893B3',
@@ -25,6 +25,7 @@ import type { SupersetTheme } from '@apache-superset/core/theme';
import type {
ButtonColorType,
ButtonProps,
ButtonStyleConfig,
ButtonStyle,
ButtonType,
ButtonVariantType,
@@ -84,14 +85,13 @@ export const getSecondaryButtonHoverStyles = (theme: SupersetTheme) => ({
},
});
const BUTTON_STYLE_MAP: Record<
ButtonStyle,
{
type?: ButtonType;
variant?: ButtonVariantType;
color?: ButtonColorType;
}
> = {
type ButtonStyleMapping = {
type?: ButtonType;
variant?: ButtonVariantType;
color?: ButtonColorType;
};
const BUTTON_STYLE_MAP: Record<ButtonStyle, ButtonStyleMapping> = {
primary: { type: 'primary', variant: 'solid', color: 'primary' },
secondary: { variant: 'filled', color: 'primary' },
tertiary: { variant: 'outlined', color: 'default' },
@@ -113,30 +113,56 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
href,
showMarginRight = true,
icon,
styleConfig,
...restProps
} = props;
const theme = useTheme();
const { fontSizeSM, fontWeightStrong } = theme;
const { fontWeightStrong } = theme;
const btnFontSize = theme.buttonFontSize ?? theme.fontSizeSM;
let height = 32;
let padding = 18;
const resolvedStyleMap: Record<ButtonStyle, ButtonStyleMapping> =
theme.buttonStyleMap
? (Object.fromEntries(
Object.entries(BUTTON_STYLE_MAP).map(([key, value]) => [
key,
{ ...value, ...theme.buttonStyleMap?.[key as ButtonStyle] },
]),
) as Record<ButtonStyle, ButtonStyleMapping>)
: BUTTON_STYLE_MAP;
let defaultHeight = theme.buttonControlHeight ?? 32;
let defaultPaddingInline = theme.buttonPaddingInline ?? 18;
let defaultBorderRadius = theme.buttonBorderRadius ?? theme.borderRadius;
if (buttonSize === 'xsmall') {
height = 22;
padding = 5;
defaultHeight = theme.buttonControlHeightXS ?? 22;
defaultPaddingInline = 5;
defaultBorderRadius = theme.buttonBorderRadius ?? theme.borderRadiusSM;
} else if (buttonSize === 'small') {
height = 30;
padding = 10;
defaultHeight = theme.buttonControlHeightSM ?? 30;
defaultPaddingInline = theme.buttonPaddingInlineSM ?? 10;
defaultBorderRadius = theme.buttonBorderRadius ?? theme.borderRadiusSM;
}
if (buttonStyle === 'link') {
padding = 4;
defaultPaddingInline = 4;
}
const resolvedStyleConfig: Required<ButtonStyleConfig> = {
controlHeight: styleConfig?.controlHeight ?? defaultHeight,
paddingInline: styleConfig?.paddingInline ?? defaultPaddingInline,
fontSize: styleConfig?.fontSize ?? btnFontSize,
fontWeight: styleConfig?.fontWeight ?? fontWeightStrong,
ctaMinWidth: styleConfig?.ctaMinWidth ?? theme.sizeUnit * 36,
ctaMinHeight: styleConfig?.ctaMinHeight ?? theme.sizeUnit * 8,
iconGap: styleConfig?.iconGap ?? theme.sizeUnit * 2,
borderRadius: styleConfig?.borderRadius ?? defaultBorderRadius,
};
const {
type: antdType = 'default',
variant,
color,
} = BUTTON_STYLE_MAP[buttonStyle ?? 'primary'];
} = resolvedStyleMap[buttonStyle ?? 'primary'] ?? BUTTON_STYLE_MAP.primary;
const element = children as ReactElement;
@@ -148,7 +174,9 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
renderedChildren = Children.toArray(children);
}
const firstChildMargin =
showMarginRight && renderedChildren.length > 1 ? theme.sizeUnit * 2 : 0;
showMarginRight && renderedChildren.length > 1
? resolvedStyleConfig.iconGap
: 0;
const effectiveButtonStyle: ButtonStyle = buttonStyle ?? 'primary';
@@ -167,11 +195,10 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
variant={variant}
danger={effectiveButtonStyle === 'danger'}
color={color}
// Static class names for embedded-dashboard CSS targeting
className={cx(
className,
'superset-button',
// A static class name containing the button style is available to
// support customizing button styles in embedded dashboards.
`superset-button-${buttonStyle}`,
{ cta: !!cta },
)}
@@ -180,12 +207,13 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
alignItems: 'center',
justifyContent: 'center',
lineHeight: 1,
fontSize: fontSizeSM,
fontWeight: fontWeightStrong,
height,
padding: `0px ${padding}px`,
minWidth: cta ? theme.sizeUnit * 36 : undefined,
minHeight: cta ? theme.sizeUnit * 8 : undefined,
fontSize: resolvedStyleConfig.fontSize,
fontWeight: resolvedStyleConfig.fontWeight,
height: resolvedStyleConfig.controlHeight,
padding: `0px ${resolvedStyleConfig.paddingInline}px`,
borderRadius: resolvedStyleConfig.borderRadius,
minWidth: cta ? resolvedStyleConfig.ctaMinWidth : undefined,
minHeight: cta ? resolvedStyleConfig.ctaMinHeight : undefined,
marginLeft: 0,
'& + .superset-button:not(.ant-btn-compact-item)': {
marginLeft: theme.sizeUnit * 2,
@@ -31,15 +31,21 @@ export type { AntdButtonProps, ButtonType, ButtonVariantType, ButtonColorType };
export type OnClickHandler = MouseEventHandler<HTMLElement>;
export type ButtonStyle =
| 'primary'
| 'secondary'
| 'tertiary'
| 'danger'
| 'link'
| 'dashed';
'primary' | 'secondary' | 'tertiary' | 'danger' | 'link' | 'dashed';
export type ButtonSize = 'default' | 'small' | 'xsmall';
export type ButtonStyleConfig = {
controlHeight?: number;
paddingInline?: number;
fontSize?: number;
fontWeight?: number;
ctaMinWidth?: number;
ctaMinHeight?: number;
iconGap?: number;
borderRadius?: number;
};
export type ButtonProps = Omit<AntdButtonProps, 'css'> & {
placement?: TooltipPlacement;
tooltip?: ReactNode;
@@ -49,4 +55,5 @@ export type ButtonProps = Omit<AntdButtonProps, 'css'> & {
cta?: boolean;
showMarginRight?: boolean;
icon?: ReactNode;
styleConfig?: ButtonStyleConfig;
};
@@ -37,13 +37,7 @@ import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-monokai';
export type CodeEditorMode =
| 'handlebars'
| 'css'
| 'json'
| 'sql'
| 'markdown'
| 'javascript'
| 'html';
'handlebars' | 'css' | 'json' | 'sql' | 'markdown' | 'javascript' | 'html';
export type CodeEditorTheme = 'light' | 'dark';
@@ -60,6 +60,11 @@ InteractiveDropdownButton.args = {
};
InteractiveDropdownButton.argTypes = {
styleConfig: {
description:
'Optional visual overrides (controlHeight, fontSize, fontWeight, boxShadow).',
control: { type: 'object' },
},
placement: {
defaultValue: 'top',
control: { type: 'select' },
@@ -0,0 +1,76 @@
/**
* 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 { fireEvent, render, waitFor } from '@superset-ui/core/spec';
import { DropdownButton } from '.';
const menuProps = { items: [{ key: '1', label: 'Item 1' }] };
test('renders without crashing when no styleConfig is provided', () => {
const { container } = render(
<DropdownButton menu={menuProps}>Click</DropdownButton>,
);
expect(container.querySelector('.ant-btn')).toBeInTheDocument();
});
test('renders without crashing with full styleConfig', () => {
const { container } = render(
<DropdownButton
menu={menuProps}
styleConfig={{
controlHeight: 40,
fontSize: 16,
fontWeight: 700,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}
>
Click
</DropdownButton>,
);
expect(container.querySelector('.ant-btn')).toBeInTheDocument();
});
test('renders tooltip when tooltip prop is provided', async () => {
const { getByText } = render(
<DropdownButton menu={menuProps} tooltip="My Tooltip">
Click
</DropdownButton>,
);
fireEvent.mouseEnter(getByText('Click'));
await waitFor(() => {
expect(document.querySelector('[role="tooltip"]')).toHaveTextContent(
'My Tooltip',
);
});
});
test('does not render tooltip wrapper when tooltip is not provided', () => {
const { container } = render(
<DropdownButton menu={menuProps}>Click</DropdownButton>,
);
expect(container.querySelector('[id$="-tooltip"]')).not.toBeInTheDocument();
});
test('passes button type to underlying Dropdown.Button', () => {
const { container } = render(
<DropdownButton menu={menuProps} type="primary">
Click
</DropdownButton>,
);
expect(container.querySelector('.ant-btn-primary')).toBeInTheDocument();
});
@@ -27,14 +27,16 @@ export const DropdownButton = ({
tooltip,
tooltipPlacement,
children,
styleConfig,
...rest
}: DropdownButtonProps) => {
const theme = useTheme();
const { type: buttonType } = rest;
// divider implementation for default (non-primary) buttons
const defaultBtnCss = css`
${(!buttonType || buttonType === 'default') &&
`.ant-dropdown-trigger {
${
(!buttonType || buttonType === 'default') &&
`.ant-dropdown-trigger {
position: relative;
&:before {
content: '';
@@ -47,7 +49,8 @@ export const DropdownButton = ({
.anticon {
vertical-align: middle;
}
}`}
}`
}
`;
const button = (
<Dropdown.Button
@@ -57,10 +60,14 @@ export const DropdownButton = ({
defaultBtnCss,
css`
.ant-btn {
height: 30px;
box-shadow: none;
font-size: ${theme.fontSizeSM}px;
font-weight: ${theme.fontWeightStrong};
height: ${
styleConfig?.controlHeight ?? theme.buttonControlHeightSM ?? 30
}px;
box-shadow: ${styleConfig?.boxShadow ?? 'none'};
font-size: ${
styleConfig?.fontSize ?? theme.buttonFontSize ?? theme.fontSizeSM
}px;
font-weight: ${styleConfig?.fontWeight ?? theme.fontWeightStrong};
}
`,
]}
@@ -85,4 +92,4 @@ export const DropdownButton = ({
return button;
};
export type { DropdownButtonProps };
export type { DropdownButtonProps, DropdownButtonStyleConfig } from './types';
@@ -21,7 +21,15 @@ import { type ComponentProps } from 'react';
import { Dropdown } from 'antd';
import type { TooltipPlacement } from '../Tooltip/types';
export type DropdownButtonStyleConfig = {
controlHeight?: number;
fontSize?: number;
fontWeight?: number;
boxShadow?: string;
};
export type DropdownButtonProps = ComponentProps<typeof Dropdown.Button> & {
tooltip?: string;
tooltipPlacement?: TooltipPlacement;
styleConfig?: DropdownButtonStyleConfig;
};
@@ -19,7 +19,7 @@
import {
cloneElement,
forwardRef,
RefObject,
ForwardedRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
@@ -54,7 +54,7 @@ export const DropdownContainer = forwardRef(
forceRender,
style,
}: DropdownContainerProps,
outerRef: RefObject<DropdownRef>,
outerRef: ForwardedRef<DropdownRef>,
) => {
const theme = useTheme();
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();
@@ -221,18 +221,22 @@ export const DynamicEditableTitle = memo(
onPressEnter={handleKeyPress}
placeholder={placeholder}
css={css`
${!canEdit &&
`&[disabled] {
${
!canEdit &&
`&[disabled] {
cursor: default;
}
`}
`
}
font-size: ${theme.fontSizeXL}px;
transition: auto;
${inputWidth &&
inputWidth > 0 &&
css`
width: ${inputWidth}px;
`}
${
inputWidth &&
inputWidth > 0 &&
css`
width: ${inputWidth}px;
`
}
`}
disabled={!canEdit}
/>
@@ -167,9 +167,11 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
{image && <ImageContainer image={image} size={size} />}
<div
css={(theme: SupersetTheme) => css`
max-width: ${containerSize === 'large'
? theme.sizeUnit * 150
: theme.sizeUnit * 100}px;
max-width: ${
containerSize === 'large'
? theme.sizeUnit * 150
: theme.sizeUnit * 100
}px;
`}
>
{title && <Title size={effectiveTextSize}>{title}</Title>}
@@ -79,7 +79,7 @@ export const LabeledErrorBoundInput = ({
isValidating ? 'validating' : hasError ? 'error' : 'success'
}
help={errorMessage || helpText}
hasFeedback={!!hasError}
hasFeedback={isValidating || !!hasError}
>
{visibilityToggle || props.name === 'password' ? (
<StyledInputPassword
@@ -21,8 +21,7 @@ export type { FormProps, FormInstance, FormItemProps } from 'antd/es/form';
export interface LabeledErrorBoundInputProps {
label?: string;
validationMethods:
| { onBlur: (value: any) => void }
| { onChange: (value: any) => void };
{ onBlur: (value: any) => void } | { onChange: (value: any) => void };
errorMessage?: string | null;
helpText?: string;
required?: boolean;
@@ -31,5 +30,6 @@ export interface LabeledErrorBoundInputProps {
id?: string;
classname?: string;
visibilityToggle?: boolean;
isValidating?: boolean;
[x: string]: any;
}
@@ -331,12 +331,21 @@ export const antdEnhancedIcons: Record<
.reduce(
(acc, key) => {
acc[key as AntdIconNames] = forwardRef<HTMLSpanElement, IconType>(
(props, ref) => (
(
{
// Forward-compat: TS 6.0 treats IconComponentProps.component as a
// different shape than BaseIconProps.component; strip it from spread
// props so our own component binding is authoritative.
component: _ignoredComponent,
...rest
},
ref,
) => (
<BaseIconComponent
ref={ref}
component={AntdIcons[key as AntdIconNames]}
fileName={key}
{...props}
{...rest}
/>
),
);
@@ -77,8 +77,7 @@ customIcons.forEach(customIcon => {
});
export type IconNameType =
| keyof typeof antdEnhancedIcons
| keyof typeof iconOverrides;
keyof typeof antdEnhancedIcons | keyof typeof iconOverrides;
type IconComponentType = Record<IconNameType, IconComponent>;
@@ -53,7 +53,7 @@ export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
overflow: hidden;
text-overflow: ellipsis;
background-color: ${backgroundColor};
border-radius: 8px;
border-radius: ${theme.labelBorderRadius ?? 8}px;
border-color: ${borderColor};
padding: 0.35em 0.8em;
line-height: 1;
@@ -27,12 +27,7 @@ import type {
export type OnClickHandler = MouseEventHandler<HTMLElement>;
export type LabelType =
| 'success'
| 'warning'
| 'error'
| 'info'
| 'default'
| 'primary';
'success' | 'warning' | 'error' | 'info' | 'default' | 'primary';
export interface LabelProps extends HTMLAttributes<HTMLSpanElement> {
key?: string;
@@ -17,10 +17,7 @@
* under the License.
*/
export type PositionOption =
| 'floating'
| 'inline'
| 'inline-centered'
| 'normal';
'floating' | 'inline' | 'inline-centered' | 'normal';
export type SizeOption = 's' | 'm' | 'l';
@@ -81,11 +81,4 @@ export type Tags = {
};
export type ContentType =
| Dashboards
| Description
| LastModified
| Owner
| Rows
| Sql
| Table
| Tags;
Dashboards | Description | LastModified | Owner | Rows | Sql | Table | Tags;

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