Compare commits

...

128 Commits

Author SHA1 Message Date
sadpandajoe
3b73f3dfbf fix(sql-lab): dedupe doubled error message on query failure
Async query failures populate both query.extra.errors (from the poll
payload merged by REFRESH_QUERIES) and query.errors (from the queryFailed
action dispatched by QueryAutoRefresh with query.extra?.errors). ResultSet
concatenated both arrays, rendering the identical error twice.

Dedupe the concatenated errors with lodash uniqWith(isEqual) so exactly one
error message is shown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:58:32 +00:00
Amitesh Gupta
3ed8889a01 fix: Added PostgreSQL 17.X to the supported database versions table in (#42280) 2026-07-22 16:24:13 -07:00
yousoph
17bd286ae9 fix(forecast): resolve time grain robustly for Prophet forecasting (#42145)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:08:57 -07:00
yousoph
32937f24d2 fix(heatmap): correct tooltip axis value lookup and percentage calculations and add tests (#41864)
Signed-off-by: yousoph <sophieyou12@gmail.com>
Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-22 15:34:10 -07:00
Elizabeth Thompson
e075133a83 fix(pandas_postprocessing): avoid FutureWarning for max/min in boxplot MINMAX (#42272) 2026-07-22 15:02:14 -07:00
Joe Li
8d034ef65c fix(plugin-chart-echarts): import the -obj locale build so time axes render (#42317)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 14:53:59 -07:00
Joe Li
641713f623 fix: Revert "chore(deps): bump echarts from 5.6.0 to 6.1.0 in /superset-frontend" (#42314)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 12:40:56 -07:00
Joe Li
40d9e7d048 fix(ag-grid-table): avoid ambiguous build query import (#42313) 2026-07-22 11:30:45 -07:00
Hans Yu
19159d58c8 refactor: engine enforce SQLAlchemy 2.0 (#42277) 2026-07-22 11:11:48 -07:00
Grégoire
894c0191d6 fix(superset_app_root): when used with oauth (#38033)
Signed-off-by: Grégoire Bellon-Gervais <gregoire.bellon-gervais@docaposte.fr>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:34:43 -07:00
Joe Li
49b4243129 test(dashboard): migrate dashboard load smoke test to Playwright (#41432)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 10:32:07 -07:00
dependabot[bot]
ac41f61d4a chore(deps): bump nanoid from 5.0.9 to 6.0.0 in /superset-frontend (#42230)
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: hainenber <dotronghai96@gmail.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 10:28:51 -07:00
dependabot[bot]
f50b7b7154 chore(deps): bump actions/setup-go from 6.5.0 to 7.0.0 (#42303)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 10:28:11 -07:00
Zack
be5744c431 fix(chart): updates counties of kenya map (#38019)
Co-authored-by: Zack Adams <zack@Zacks-Laptop.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-07-22 10:17:29 -07:00
SkinnyPigeon
8c5d465a65 feat(datasets): add RLS filter indicator badge to dataset list and explore view (#38807)
Co-authored-by: Evan <evan@preset.io>
2026-07-22 10:07:52 -07:00
Evan Rusackas
c5935b6904 feat(table/pivot-table): correct non-additive totals/subtotals via DB rollup [SIP-216] (#41184)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-07-22 10:06:05 -07:00
Evan Rusackas
10ff470702 fix(ag-grid-table): respect row limit with server pagination (#41346)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:03:33 -07:00
Amin Ghadersohi
1c0f259e38 refactor(mcp): dedupe list-tool schemas and delete dead middleware (#41923) 2026-07-22 12:29:59 -04:00
Alexandru Soare
b8f3b768d4 fix(sqllab): Fall back to empty editor tab when linked query no longer exists (#42241) 2026-07-22 15:30:42 +03:00
dependabot[bot]
6e1954fd51 chore(deps): bump ws from 8.21.0 to 8.21.1 in /superset-websocket (#42301)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 02:21:25 -07:00
dependabot[bot]
248f6e1064 chore(deps): bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0 (#42302)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 02:21:21 -07:00
dependabot[bot]
46c5e30533 chore(deps-dev): bump oxlint from 1.73.0 to 1.74.0 in /superset-frontend (#42304)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 02:21:17 -07:00
Mehmet Salih Yavuz
b71b58e821 fix(explore): render stopped state when a chart query is cancelled (#42270) 2026-07-22 11:14:08 +03:00
Damian Pendrak
1392fbc9b2 fix(sql): validate Custom SQL metric has an aggregate under GROUP BY (#42199) 2026-07-22 09:28:57 +02:00
Mafi
a086b2eae9 fix(clickhouse): retry adhoc column type probe with comment-safe SQL (#42298)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
2026-07-22 02:39:49 -04:00
Pawan
788165b5bb fix(frontend): jest testRegex matches Windows path separators (#41863) 2026-07-21 20:32:33 -07:00
dependabot[bot]
60a7c18204 chore(deps): bump svgo from 3.3.3 to 3.3.4 in /docs (#42286)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 17:55:25 -07:00
dependabot[bot]
db1959b9b6 chore(deps): bump fast-uri from 3.1.2 to 3.1.4 in /superset-frontend/cypress-base (#42292)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 17:55:20 -07:00
dependabot[bot]
936b8a84ae chore(deps): bump fast-uri from 3.1.3 to 3.1.4 in /superset-frontend (#42293)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 17:55:15 -07:00
dependabot[bot]
37ef2f381e chore(deps): bump fast-uri from 3.1.3 to 3.1.4 in /docs (#42294)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 17:55:11 -07:00
Amin Ghadersohi
cae61b51e1 fix(cache): QueryCacheManager.get() should fail open on cache backend errors (#42252) 2026-07-21 20:05:00 -04:00
dependabot[bot]
9c08240fdc chore(deps): bump dompurify from 3.4.11 to 3.4.12 in /docs (#42287)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 16:44:49 -07:00
Igor Khrol
072ff01406 fix: disable recaptcha for LDAP authentication (#36857)
Co-authored-by: Igor Khrol <khroliz@Igors-MacBook-Pro-2.local>
2026-07-21 16:31:12 -07:00
Keonik
8b1ef3a4cb feat(helm): Add annotations to secrets manifests (#40803)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:27:55 -07:00
JUST.in DO IT
d62dbdb694 fix(chat): prevent main content reload when toggling chat panel (#42143) 2026-07-21 16:27:29 -07:00
Aleksey
2ddea33bb6 fix(models): inline calculated column expression in raw-records queries (#34784) (#42040)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-21 16:23:59 -07:00
Ville Brofeldt
15d7d36580 chore: remove deprecated from_dttm and to_dttm jinja vars (#42279) 2026-07-21 15:07:36 -07:00
dependabot[bot]
ab8e23d874 chore(deps-dev): bump svgo from 3.3.3 to 3.3.4 in /superset-frontend (#42285)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 14:45:12 -07:00
Jean Massucatto
1b882c1f47 test(explore): cover annotation layer link app-root prefix in subdire… (#41997) 2026-07-21 14:05:30 -07:00
dependabot[bot]
8329481c2e chore(deps-dev): bump the typescript-eslint group across 1 directory with 2 updates (#42258)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 14:04:48 -07:00
dependabot[bot]
c95c8086f5 chore(deps-dev): bump webpack-bundle-analyzer from 5.3.0 to 5.3.1 in /superset-frontend (#42261)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 14:04:44 -07:00
dependabot[bot]
cfac785541 chore(deps): bump fuse.js from 7.4.2 to 7.5.0 in /superset-frontend (#42262)
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-21 14:04:40 -07:00
dependabot[bot]
b97a7f5f94 chore(deps-dev): bump fast-uri from 3.1.2 to 3.1.4 in /superset-embedded-sdk (#42282)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 14:04:30 -07:00
Amin Ghadersohi
e5c418a187 fix(mcp): normalize bracket-shorthand time ranges in query_dataset (#42144) 2026-07-21 15:42:04 -04:00
Michael S. Molina
34ebe3d22a feat(extensions): add a Storage API for extensions (#39171)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Benyovszky Máté <benyovszky.mate.work@outlook.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 16:23:56 -03:00
SBIN2010
0dd06f3b7b fix: dashboard filter regress (#35218)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-21 21:04:04 +03:00
Hans Yu
40a13883ea refactor: do not cascade to SqlMetric (#42221)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 10:26:01 -07:00
Evan Rusackas
f2967264d2 fix(ci): diff pre-commit changed files against the live base branch tip (#42152)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 10:20:57 -07:00
Evan Rusackas
706a65cb02 fix(docs): allow *.run.app in CSP for the Kapa widget backend (#42276)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 10:18:13 -07:00
Evan Rusackas
f9378baba8 test(sqla): add the jinja orderby calculated column to the session (#42274)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-21 10:16:29 -07:00
Evan Rusackas
e1a265447f fix(charts): use chart name in CSV/XLSX/zip export filenames (#42193)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 10:16:17 -07:00
Amin Ghadersohi
f999afaac0 fix(mcp): coerce float rowcount to int in ChartData to prevent PydanticSerializationError (#42140) 2026-07-21 10:10:33 -07:00
Elizabeth Thompson
1a862476e1 fix(dashboard): add cacheKey, dashboardId, and format context to screenshot download error logs (#42121)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-21 10:10:14 -07:00
Taras Pashkevych
df34b5d277 fix(drill-detail): paginate Elasticsearch samples via engine cursor (#39509) 2026-07-21 10:09:28 -07:00
Evan Rusackas
b362d36019 chore: remove deck.gl JavaScript tooltip controls and ENABLE_JAVASCRIPT_CONTROLS (#42126)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:01:25 -07:00
Evan Rusackas
666b6805c4 test(sql): prove Oracle GROUP BY stays explicit for virtual-dataset charts (#42255)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 09:59:34 -07:00
dependabot[bot]
3fa7cc5986 chore(deps): bump react-diff-viewer-continued from 4.3.0 to 4.4.0 in /superset-frontend (#42263)
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-21 09:47:05 -07:00
dependabot[bot]
7f4d151a37 chore(deps): bump body-parser from 2.2.1 to 2.3.0 in /superset-websocket/utils/client-ws-app (#42264)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 09:46:56 -07:00
dependabot[bot]
5efbd5b157 chore(deps-dev): bump brace-expansion from 5.0.6 to 5.0.7 in /superset-websocket (#42267)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 09:46:48 -07:00
Hugh A. Miles II
00cb3037e7 feat(dashboard): export dashboard chart data to Excel (async, S3 + email) (#41133) 2026-07-21 12:17:00 -04:00
Hans Yu
6c58afe6ac refactor: do not cascade to TableColumn (#42222) 2026-07-21 09:00:47 -07:00
Evan Rusackas
f751716867 fix(trino): quote nested ROW field paths per-segment in physical column expressions (#41946)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 04:27:24 -07:00
dependabot[bot]
bafe5ad9ba chore(deps): bump actions/setup-java from 5.5.0 to 5.6.0 (#42260)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 01:15:32 -07:00
dependabot[bot]
a1d4cda0cb chore(deps-dev): bump typescript-eslint from 8.63.0 to 8.64.0 in /docs in the typescript-eslint group (#42259)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 01:15:28 -07:00
Evan Rusackas
2d3cbc162e fix(db_engine_specs): map var_string to STRING for MySQL/StarRocks (#41868)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 00:11:46 -07:00
Evan Rusackas
e4005f02c6 fix(sqla): render Jinja templates in calculated columns used via orderby adhoc metrics (#41870)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 00:11:26 -07:00
jesperct
8296fe8ce4 fix(home): restore navigation for saved query cards (#41912) 2026-07-20 17:33:32 -07:00
Elizabeth Thompson
4b659da5c4 fix(reports): positive per-tile chart readiness check for tiled screenshots (#42119)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: yousoph <sophieyou12@gmail.com>
2026-07-20 17:31:45 -07:00
worker24h
b4529c2654 feat: add distance unit (#24444)
Co-authored-by: xuxiaobing <xuxiaobing@neolix.cn>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-07-20 16:25:48 -07:00
Evan Rusackas
034823e99b chore: replace deprecated datetime.utcnow()/utcfromtimestamp() (#41502)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:37:57 -07:00
Evan Rusackas
abd6dc8e44 refactor: set cascade_backrefs=False for SavedQuery (#42212)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 15:37:28 -07:00
Evan Rusackas
b3757870cc fix(sqllab): apply SQL_QUERY_MUTATOR in SQL Lab when MUTATE_AFTER_SPLIT is set (#41127)
Co-authored-by: Lucas Wolkersdorfer <lucas.wolkersdorfer@rise-world.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:36:45 -07:00
dependabot[bot]
158e321992 chore(deps): bump axios from 1.16.0 to 1.18.1 in /superset-frontend (#42251)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 11:45:40 -07:00
Evan Rusackas
590357731b feat(ci): cancel CI runs when hold label applied, re-run on removal (#40387)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-07-20 10:35:26 -07:00
Elizabeth Thompson
64053271e8 fix(views): remove stray space in @deprecated new_target log message (#42218) 2026-07-20 10:35:02 -07:00
Matthew Chiang
940e2c34a2 fix(Calendar Heatmap): Add Back chart options for Calendar Heatmap (#26230)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-07-20 10:32:35 -07:00
Đỗ Trọng Hải
d65a816b39 chore(lint): toggle import/no-named-as-default to error level + fix all violations (#42214)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-20 10:22:15 -07:00
Evan Rusackas
48faca5b8d refactor: set cascade_backrefs=False for SqlaTable (#42213)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:22:00 -07:00
Evan Rusackas
9178faf9b1 refactor: set cascade_backrefs=False for Query (#42210)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:20:09 -07:00
PRATHAMESH HUKKERI
15bc73facf test(table): add regression test for pagination reset on cell click (#42155)
Co-authored-by: Prathamesh Hukkeri <prathamesh04@users.noreply.github.com>
2026-07-20 10:15:29 -07:00
Evan Rusackas
e0d0eb6d02 fix(build): deterministic echarts locale imports; fingerprint loaded config (#42154)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:13:58 -07:00
Đỗ Trọng Hải
1f81b700c1 chore(ci): upgrade GHA runner image to Ubuntu 26.04 (#42191)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: David <39565245+dmunozv04@users.noreply.github.com>
Co-authored-by: Hans Yu <hans.yu@outlook.de>
Co-authored-by: Shaitan <105581038+sha174n@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Rehan Islam <2025eb01502@online.bits-pilani.ac.in>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J0s3-H3nr1qu3 <hareboom@gmail.com>
Co-authored-by: José Teixeira <jose.teixeira@douroeci.com>
2026-07-20 09:43:19 -07:00
Jean Massucatto
3a3f087869 fix(native-filters): support double-quote escaping for comma in filte… (#39068)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:42:52 -07:00
Evan Rusackas
f03a66db9c fix(dataset): apply HOURS OFFSET to all temporal columns, not just the time column (#41489)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:23:20 -07:00
Adolfo Carvalho
515dfef955 fix(dashboard import): Importing existing dashboard via UI won't update charts and datasets (#34880)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:22:06 -07:00
Evan Rusackas
5a17ae1224 chore: drop Python 3.10 support, require Python 3.11+ (#42045)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-07-20 08:16:22 -07:00
Mehmet Salih Yavuz
c1e660fac8 fix(databases): reset import file entry after invalid file error (#42240) 2026-07-20 18:06:44 +03:00
dependabot[bot]
19ca088de6 chore(deps): bump baseline-browser-mapping from 2.10.42 to 2.10.43 in /docs (#42227)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 07:44:06 -07:00
dependabot[bot]
57dc3c0208 chore(deps): bump antd from 6.5.0 to 6.5.1 in /superset-frontend (#42234)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 21:42:51 +07:00
dependabot[bot]
ceffbe80f9 chore(deps): bump hot-shots from 16.0.0 to 17.0.0 in /superset-websocket (#42236)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 21:42:00 +07:00
dependabot[bot]
eed41b6435 chore(deps): bump actions/setup-node from 6.4.0 to 7.0.0 (#42229)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 21:33:36 +07:00
J0s3-H3nr1qu3
277c814c5a fix(i18n): review and complete Portuguese (pt_PT) translation catalog (#42137)
Co-authored-by: José Teixeira <jose.teixeira@douroeci.com>
2026-07-20 14:37:43 +01:00
dependabot[bot]
7e63b29741 chore(deps): bump antd from 6.5.0 to 6.5.1 in /docs (#42226)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:56:10 -07:00
dependabot[bot]
b5ae7cbd0d chore(deps): bump caniuse-lite from 1.0.30001803 to 1.0.30001805 in /docs (#42228)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:56:03 -07:00
dependabot[bot]
e044d7783a chore(deps): bump dompurify from 3.4.11 to 3.4.12 in /superset-frontend (#42231)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:55:59 -07:00
dependabot[bot]
9e8b9ac8cf chore(deps-dev): bump tsx from 4.23.0 to 4.23.1 in /superset-frontend (#42232)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:55:55 -07:00
dependabot[bot]
19588826c8 chore(deps-dev): bump baseline-browser-mapping from 2.10.42 to 2.10.43 in /superset-frontend (#42233)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:55:50 -07:00
dependabot[bot]
8b415502cf chore(deps): bump chrono-node from 2.9.1 to 2.10.0 in /superset-frontend (#42235)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:55:43 -07:00
Rehan Islam
09148d0af6 fix(time-table): correctly sort computed columns (#41976) 2026-07-19 20:56:53 -07:00
Shaitan
3e40bebba8 fix(import): enforce write permission for objects created by legacy import paths (#42205)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:50:58 -07:00
Hans Yu
36dbff091f refactor: do not cascade to TaggedObject (#42223) 2026-07-19 20:18:53 -07:00
David
9e9b7c4bbd feat(ci): run helm-unittest on pull requests (#42206) 2026-07-20 07:36:19 +07:00
dependabot[bot]
42a2aede78 chore(deps): bump pandas from 2.1.4 to 2.3.3 (#42192)
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: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 15:42:02 -07:00
lguichard78
c4d50472a9 fix(login): oauth and ldap login ignoring superset_app_root (#34657) 2026-07-18 14:58:22 -07:00
Younsung Lee
cd045886d0 feat(helm): add Gateway API HTTPRoute support (#41073)
Signed-off-by: younsl <cysl@kakao.com>
2026-07-18 14:57:02 -07:00
Jean Massucatto
751f5eb663 fix(dashboard): disable Discard button when there are no unsaved changes (#40832)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-18 14:15:17 -07:00
serdukow
4e098b6f38 fix(ag-grid-table): use t('Main') for time comparison column keys to fix i18n (#40681)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-18 13:00:53 -07:00
David
5ee4a81906 feat(helm): add Superset MCP server templates (#40507) 2026-07-18 12:59:40 -07:00
Mike Bridge
caf017bd0b feat(versioning): cross-entity version activity view (#41076)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 11:06:54 -07:00
jenwitteng
a83171bce6 fix(database): capture result metadata after fetch (#42127)
Signed-off-by: Jenwit Amonpongitsara <jenwit.amonpongitsara@agoda.com>
2026-07-18 10:54:36 -07:00
Đỗ Trọng Hải
6c13ab6657 feat(ci): optimize Storybook testing in CI by replacing http-server with Python3 static server (#41956)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-18 10:53:54 -07:00
Đỗ Trọng Hải
b9ede492b7 chore(lint): toggle import/named and import/export lint rules to error level (#42189)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-18 10:53:40 -07:00
Gagandeep Singh Choudhary
fb29db6119 fix(sqllab): preserve database state on SET_DATABASES (#41281)
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 10:52:45 -07:00
David
0a66e4ea2d fix(helm): support initContainers in supersetWebsockets (#42201) 2026-07-18 10:29:42 -07:00
AYUSHI RAI
3e0f9e60c0 fix(charts): preserve minute/second precision in X-axis time labels f… (#41505)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-18 10:28:03 -07:00
Durgaprasad M L
ff1cf5f24e fix(table): hide search dropdown when search box is disabled (#41772) 2026-07-18 10:23:54 -07:00
dependabot[bot]
023b60c8f0 chore(deps-dev): bump pandas-gbq from 0.19.1 to 0.35.0 (#42161)
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-18 07:23:17 -07:00
dependabot[bot]
2ada286ee9 chore(deps-dev): update pydynamodb requirement from >=0.4.2 to >=0.8.2 (#42162)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 07:23:13 -07:00
dependabot[bot]
42e5640f60 chore(deps-dev): update nzalchemy requirement from <11.2,>=11.0.2 to >=11.1.2,<11.2 (#42164)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 07:23:09 -07:00
dependabot[bot]
74703906df chore(deps): bump dom-to-image-more from 3.10.0 to 3.10.2 in /superset-frontend (#42180)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 21:15:23 +07:00
dependabot[bot]
3c23394675 chore(deps-dev): bump eslint from 10.6.0 to 10.7.0 in /superset-websocket (#42159)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 16:12:08 +07:00
dependabot[bot]
b6ce28ff72 chore(deps): bump typing-extensions from 4.15.0 to 4.16.0 (#42165)
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-18 16:11:46 +07:00
dependabot[bot]
79eff6b6f1 chore(deps): bump greenlet from 3.5.1 to 3.5.3 (#42166)
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-18 16:08:28 +07:00
dependabot[bot]
a58012fc0f chore(deps): bump the docusaurus-openapi group in /docs with 2 updates (#42195)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 16:05:39 +07:00
dependabot[bot]
dab5f30842 chore(deps): bump @docusaurus/faster from 3.10.1 to 3.10.2 in /docs (#42179)
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-18 15:55:18 +07:00
dependabot[bot]
8299e7f87c chore(deps-dev): bump storybook from 10.4.6 to 10.5.0 in /superset-frontend (#42185)
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: hainenber <dotronghai96@gmail.com>
2026-07-18 15:41:22 +07:00
dependabot[bot]
f6acf68dfa chore(deps): bump react-intersection-observer from 10.0.3 to 10.1.0 in /superset-frontend (#42181)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 13:31:08 +07:00
dependabot[bot]
cce77b42f6 chore(deps-dev): bump @formatjs/intl-durationformat from 0.10.16 to 0.10.17 in /superset-frontend (#42184)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-18 13:11:17 +07:00
554 changed files with 38386 additions and 13169 deletions

View File

@@ -50,9 +50,8 @@ body:
attributes:
label: Python version
options:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- Not applicable
- I don't know
validations:

View File

@@ -65,6 +65,11 @@ updates:
lumagl:
patterns:
- "@luma.gl/*"
storybook:
patterns:
- "@storybook/*"
- "storybook"
- "eslint-plugin-storybook"
open-pull-requests-limit: 30
versioning-strategy: increase
cooldown:
@@ -99,6 +104,22 @@ updates:
directory: "/docs/"
schedule:
interval: "daily"
groups:
storybook:
patterns:
- "@storybook/*"
- "storybook"
docusaurus:
patterns:
- "@docusaurus/*"
docusaurus-openapi:
patterns:
- "docusaurus-plugin-openapi-docs"
- "docusaurus-theme-openapi-docs"
typescript-eslint:
patterns:
- "@typescript-eslint/*"
- "typescript-eslint"
open-pull-requests-limit: 10
versioning-strategy: increase
cooldown:

View File

@@ -23,7 +23,7 @@ on:
jobs:
bump-python-package:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
actions: write
contents: write
@@ -42,7 +42,7 @@ jobs:
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.10"
python-version: "3.11"
- name: Install uv
run: pip install uv

View File

@@ -19,7 +19,7 @@ concurrency:
jobs:
check-python-deps:
runs-on: ubuntu-22.04
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

View File

@@ -19,7 +19,7 @@ concurrency:
jobs:
check_db_migration_conflict:
name: Check DB migration conflict
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: read
pull-requests: write

View File

@@ -16,7 +16,7 @@ concurrency:
jobs:
changes:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
@@ -42,7 +42,7 @@ jobs:
# analysis runners don't spin up. push/schedule runs always proceed:
# the change-detector returns "all changed" for non-PR events.
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 30
permissions:
actions: read

View File

@@ -24,7 +24,7 @@ permissions:
jobs:
dependency-review:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- name: "Checkout Repository"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -48,7 +48,7 @@ jobs:
python-dependency-liccheck:
# NOTE: Configuration for liccheck lives in our pyproject.yml.
# You cannot use a liccheck.ini file in this workflow.
runs-on: ubuntu-22.04
runs-on: ubuntu-26.04
steps:
- name: "Checkout Repository"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

View File

@@ -19,7 +19,7 @@ concurrency:
jobs:
changes:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
@@ -40,14 +40,14 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
setup_matrix:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 5
outputs:
matrix_config: ${{ steps.set_matrix.outputs.matrix_config }}
steps:
- id: set_matrix
run: |
MATRIX_CONFIG=$(if [ "${{ github.event_name }}" == "pull_request" ]; then echo '["dev", "lean"]'; else echo '["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]'; fi)
MATRIX_CONFIG=$(if [ "${{ github.event_name }}" == "pull_request" ]; then echo '["dev", "lean"]'; else echo '["dev", "lean", "websocket", "dockerize", "py311", "py312"]'; fi)
echo "matrix_config=${MATRIX_CONFIG}" >> $GITHUB_OUTPUT
echo $GITHUB_OUTPUT
@@ -58,7 +58,7 @@ jobs:
needs.changes.outputs.python == 'true' ||
needs.changes.outputs.frontend == 'true' ||
needs.changes.outputs.docker == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 60
strategy:
matrix:
@@ -123,13 +123,20 @@ jobs:
# pull timeouts, 504/401 on push, ECONNRESET) that otherwise fail
# the whole job. buildx reuses the buildkit layer cache from the
# failed attempt, so a retry mostly re-does just the failed push.
#
# supersetbot's "dev"/"lean" presets pin their own --build-arg
# PY_VER, which lands ahead of --extra-flags on the assembled
# buildx command line; docker/buildx keeps the last value for a
# repeated --build-arg key, so appending PY_VER here overrides
# supersetbot's pin and keeps the build on the Dockerfile's own
# supported Python version.
for attempt in 1 2 3; do
if supersetbot docker \
$PUSH_OR_LOAD \
--preset "$BUILD_PRESET" \
--context "$EVENT" \
--context-ref "$RELEASE" $FORCE_LATEST \
--extra-flags "--build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
--extra-flags "--build-arg PY_VER=3.11.14-slim-trixie --build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
$PLATFORM_ARG; then
break
fi
@@ -173,7 +180,7 @@ jobs:
# goal is to check that building the latest image works, not required for all PR pushes
needs: changes
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.changes.outputs.docker == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 30
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"

View File

@@ -15,7 +15,7 @@ jobs:
# gate on. Restrict to the canonical repo: forks cannot mint a valid OIDC
# token for this package and must not publish.
if: github.repository == 'apache/superset'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: read
id-token: write # required for npm trusted publishing (OIDC)
@@ -31,7 +31,7 @@ jobs:
# token, which makes npm attempt token auth and skip the OIDC
# trusted-publishing exchange. With no .npmrc auth line, npm authenticates
# via OIDC against the default registry (registry.npmjs.org).
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-embedded-sdk/.nvmrc"
- run: npm ci

View File

@@ -16,7 +16,7 @@ concurrency:
jobs:
embedded-sdk-test:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
defaults:
run:
working-directory: superset-embedded-sdk
@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-embedded-sdk/.nvmrc"
registry-url: "https://registry.npmjs.org"

View File

@@ -11,7 +11,7 @@ permissions:
jobs:
config:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
@@ -29,7 +29,7 @@ jobs:
needs: config
if: needs.config.outputs.has-secrets
name: Generate Report
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -37,7 +37,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: "temurin"
java-version: "11"

View File

@@ -19,7 +19,7 @@ concurrency:
jobs:
validate-all-ghas:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: read
# Required for the zizmor action to upload its SARIF results to
@@ -32,7 +32,7 @@ jobs:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "20"
@@ -46,4 +46,4 @@ jobs:
run: bash .github/workflows/github-action-validator.sh
- name: Check for security issues on GHA workflows
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0

115
.github/workflows/hold-label-ci-gate.yml vendored Normal file
View File

@@ -0,0 +1,115 @@
# 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.
name: Hold Label CI Gate
on:
pull_request:
types: [labeled, unlabeled]
permissions: {}
jobs:
cancel-on-hold:
name: Cancel CI runs when hold label applied
if: github.event.action == 'labeled' && startsWith(github.event.label.name, 'hold')
runs-on: ubuntu-26.04
permissions:
actions: write
pull-requests: read
steps:
- name: Cancel in-progress workflow runs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const sha = context.payload.pull_request.head.sha;
const { data: { workflow_runs: runs } } =
await github.rest.actions.listWorkflowRunsForRepo({
owner,
repo,
head_sha: sha,
per_page: 100,
});
const active = runs.filter(r =>
['in_progress', 'queued', 'waiting', 'requested', 'pending'].includes(r.status)
);
core.info(`Found ${active.length} active run(s) to cancel for SHA ${sha}`);
for (const run of active) {
try {
await github.rest.actions.cancelWorkflowRun({
owner,
repo,
run_id: run.id,
});
core.info(`Cancelled run ${run.id} (${run.name})`);
} catch (err) {
core.warning(`Could not cancel run ${run.id}: ${err.message}`);
}
}
rerun-on-unhold:
name: Re-run CI when hold label removed
if: github.event.action == 'unlabeled' && startsWith(github.event.label.name, 'hold')
runs-on: ubuntu-24.04
permissions:
actions: write
pull-requests: read
steps:
- name: Re-trigger cancelled workflow runs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const sha = context.payload.pull_request.head.sha;
// Check that no other hold* labels remain on the PR
const labels = context.payload.pull_request.labels.map(l => l.name);
const stillHeld = labels.some(l => l.startsWith('hold'));
if (stillHeld) {
core.info('PR still has a hold label — skipping re-run.');
return;
}
const { data: { workflow_runs: runs } } =
await github.rest.actions.listWorkflowRunsForRepo({
owner,
repo,
head_sha: sha,
per_page: 100,
});
const cancelled = runs.filter(r => r.conclusion === 'cancelled');
core.info(`Found ${cancelled.length} cancelled run(s) to re-trigger for SHA ${sha}`);
for (const run of cancelled) {
try {
await github.rest.actions.reRunWorkflow({
owner,
repo,
run_id: run.id,
});
core.info(`Re-triggered run ${run.id} (${run.name})`);
} catch (err) {
core.warning(`Could not re-run ${run.id}: ${err.message}`);
}
}

View File

@@ -9,7 +9,7 @@ on:
jobs:
superbot-orglabel:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: read
pull-requests: write

View File

@@ -12,7 +12,7 @@ jobs:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
with:

View File

@@ -6,7 +6,7 @@ on:
jobs:
latest-release:
name: Add/update tag to new release
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: write

View File

@@ -15,7 +15,7 @@ concurrency:
jobs:
license_check:
name: License Check
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -23,7 +23,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: "temurin"
java-version: "11"

View File

@@ -17,7 +17,7 @@ concurrency:
jobs:
check-hold-label:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- name: Check for 'hold' label
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0

View File

@@ -21,7 +21,7 @@ concurrency:
jobs:
lint-check:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: read
pull-requests: write

View File

@@ -23,7 +23,7 @@ concurrency:
jobs:
pre-commit:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 20
strategy:
matrix:
@@ -47,13 +47,13 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Setup Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.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.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "superset-frontend/.nvmrc"
cache: "npm"
@@ -81,7 +81,6 @@ jobs:
id: changed_files
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
@@ -95,7 +94,15 @@ jobs:
# Resolve the commit to diff against.
base=""
if [ "${EVENT_NAME}" = "pull_request" ]; then
base="${BASE_SHA}"
# HEAD is the PR merge commit, so its first parent is the current
# tip of the base branch. github.event.pull_request.base.sha is
# NOT that: GitHub freezes it at PR creation, so on a long-lived
# PR it points at the original branch point and the diff picks up
# all of the base branch's churn since then — thousands of paths,
# enough for the CHANGED_FILES env var below to exceed the
# kernel's per-variable size limit and kill the step with
# "Argument list too long" before bash even starts.
base="$(git rev-parse HEAD^1 2>/dev/null || true)"
elif [ -n "${BEFORE_SHA:-}" ] && \
[ "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]; then
base="${BEFORE_SHA}"
@@ -114,6 +121,15 @@ jobs:
# Files present in HEAD that changed since the base (drop deletions).
files="$(git diff --name-only --diff-filter=ACMRT "${base}...HEAD")"
# Env vars have a hard per-variable size limit (E2BIG at step
# start). A PR that legitimately touches thousands of files is
# better served by --all-files anyway.
if [ "$(printf '%s' "${files}" | wc -c)" -gt 100000 ]; then
echo "::notice::Changed-file list too large to pass via env; falling back to --all-files."
echo "mode=all" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -z "${files}" ]; then
echo "mode=none" >> "$GITHUB_OUTPUT"
else

View File

@@ -11,7 +11,7 @@ permissions:
jobs:
config:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
@@ -29,7 +29,7 @@ jobs:
needs: config
if: needs.config.outputs.has-secrets
name: Bump version and publish package(s)
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: write
steps:
@@ -50,7 +50,7 @@ jobs:
- name: Install Node.js
if: env.HAS_TAGS
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-frontend/.nvmrc"

View File

@@ -40,7 +40,7 @@ concurrency:
jobs:
config:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
latest-release: ${{ steps.latest.outputs.tag }}
@@ -93,12 +93,12 @@ jobs:
needs: config
if: needs.config.outputs.has-secrets == '1'
name: docker-rebuild
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
strategy:
# Mirror the same matrix the release publisher uses so every variant
# operators consume from Docker Hub gets the refreshed base.
matrix:
build_preset: ["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
build_preset: ["dev", "lean", "websocket", "dockerize", "py311", "py312"]
fail-fast: false
steps:
- name: "Checkout release tag: ${{ needs.config.outputs.latest-release }}"
@@ -117,7 +117,7 @@ jobs:
build: "true"
- name: Use Node.js 20
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20
@@ -155,7 +155,7 @@ jobs:
notify-on-failure:
needs: [config, docker-rebuild]
if: failure() && needs.config.outputs.has-secrets == '1'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: read
issues: write

View File

@@ -19,7 +19,7 @@ concurrency:
jobs:
test-load-examples:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
env:
PYTHONPATH: ${{ github.workspace }}
SUPERSET_CONFIG: tests.integration_tests.superset_test_config

View File

@@ -33,7 +33,7 @@ permissions:
jobs:
config:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
@@ -57,7 +57,7 @@ jobs:
(github.event_name != 'workflow_run' ||
github.event.workflow_run.head_repository.full_name == github.repository)
name: Build & Deploy
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -66,12 +66,12 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./docs/.nvmrc"
- name: Setup Python
uses: ./.github/actions/setup-backend/
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: "zulu"
java-version: "21"

View File

@@ -67,7 +67,7 @@ jobs:
# Build docs when PR changes docs/** (uses committed databases.json)
if: github.event_name == 'pull_request'
name: Build (PR trigger)
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
defaults:
run:
working-directory: docs
@@ -78,7 +78,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./docs/.nvmrc"
- name: yarn install
@@ -106,7 +106,7 @@ jobs:
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name == github.repository
name: Build (after integration tests)
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
defaults:
run:
working-directory: docs
@@ -118,7 +118,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./docs/.nvmrc"
- name: yarn install

View File

@@ -28,7 +28,7 @@ concurrency:
jobs:
changes:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
@@ -50,8 +50,7 @@ jobs:
cypress-matrix:
needs: changes
if: (needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true') && github.event.pull_request.draft == false
# Somehow one test flakes on 24.04 for unknown reasons, this is the only GHA left on 22.04
runs-on: ubuntu-22.04
runs-on: ubuntu-26.04
timeout-minutes: 30
permissions:
contents: read
@@ -128,7 +127,7 @@ jobs:
with:
run: testdata
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"
@@ -173,7 +172,7 @@ jobs:
playwright-tests:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-22.04
runs-on: ubuntu-26.04
timeout-minutes: 30
permissions:
contents: read
@@ -238,7 +237,7 @@ jobs:
with:
run: playwright_testdata
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"
@@ -301,7 +300,7 @@ jobs:
cypress-matrix-required:
needs: [changes, cypress-matrix]
if: always()
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
@@ -323,7 +322,7 @@ jobs:
playwright-tests-required:
needs: [changes, playwright-tests]
if: always()
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:

View File

@@ -19,7 +19,7 @@ concurrency:
jobs:
test-superset-extensions-cli-package:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 30
strategy:
matrix:

View File

@@ -21,7 +21,7 @@ permissions:
jobs:
frontend-build:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 30
outputs:
should-run: ${{ steps.check.outputs.frontend }}
@@ -51,7 +51,7 @@ jobs:
git show -s --format=raw HEAD
docker buildx build \
-t $TAG \
--cache-from=type=registry,ref=apache/superset-cache:3.10-slim-trixie \
--cache-from=type=registry,ref=apache/superset-cache:3.11-slim-trixie \
--target superset-node-ci \
.
@@ -74,7 +74,7 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
fail-fast: false
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 20
steps:
- name: Download Docker Image Artifact
@@ -104,7 +104,7 @@ jobs:
report-coverage:
needs: [sharded-jest-tests]
if: needs.frontend-build.outputs.should-run == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 15
permissions:
id-token: write
@@ -146,7 +146,7 @@ jobs:
lint-frontend:
needs: frontend-build
if: needs.frontend-build.outputs.should-run == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 20
steps:
- name: Download Docker Image Artifact
@@ -171,7 +171,7 @@ jobs:
validate-frontend:
needs: frontend-build
if: needs.frontend-build.outputs.should-run == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 20
steps:
- name: Download Docker Image Artifact
@@ -191,7 +191,7 @@ jobs:
test-storybook:
needs: frontend-build
if: needs.frontend-build.outputs.should-run == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 25
steps:
- name: Download Docker Image Artifact

View File

@@ -19,7 +19,7 @@ concurrency:
jobs:
lint-test:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -31,7 +31,7 @@ jobs:
- name: Set up Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v3.16.4
version: v3.21.3
- name: Setup Python
uses: ./.github/actions/setup-backend/
@@ -59,3 +59,9 @@ jobs:
CT_LINT_CONF: lintconf.yaml
CT_SINCE: HEAD
CT_CHART_REPOS: bitnami=https://charts.bitnami.com/bitnami
- name: Set up helm-unittest
run: helm plugin install https://github.com/helm-unittest/helm-unittest --version v1.1.1
- name: Run helm-unittest
run: helm unittest helm/superset

View File

@@ -20,7 +20,7 @@ on:
jobs:
release:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: write
pull-requests: write

View File

@@ -24,7 +24,7 @@ concurrency:
jobs:
changes:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
@@ -48,7 +48,7 @@ jobs:
playwright-tests-experimental:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-22.04
runs-on: ubuntu-26.04
timeout-minutes: 30
continue-on-error: true
permissions:
@@ -114,7 +114,7 @@ jobs:
with:
run: playwright_testdata
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"

View File

@@ -20,7 +20,7 @@ concurrency:
jobs:
changes:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
@@ -41,7 +41,7 @@ jobs:
test-mysql:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 45
permissions:
id-token: write
@@ -127,7 +127,7 @@ jobs:
test-postgres:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 45
permissions:
id-token: write
@@ -188,7 +188,7 @@ jobs:
test-sqlite:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 45
permissions:
id-token: write
@@ -243,7 +243,7 @@ jobs:
test-postgres-required:
needs: [changes, test-postgres]
if: always()
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- name: Check test-postgres result

View File

@@ -16,7 +16,7 @@ concurrency:
jobs:
changes:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
@@ -37,7 +37,7 @@ jobs:
test-postgres-presto:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 45
permissions:
id-token: write
@@ -100,7 +100,7 @@ jobs:
test-postgres-hive:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 45
permissions:
id-token: write

View File

@@ -21,7 +21,7 @@ concurrency:
jobs:
changes:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
@@ -42,7 +42,7 @@ jobs:
unit-tests:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 30
permissions:
id-token: write
@@ -99,7 +99,7 @@ jobs:
unit-tests-required:
needs: [changes, unit-tests]
if: always()
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- name: Check unit-tests result

View File

@@ -20,7 +20,7 @@ permissions:
jobs:
post-comment:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
# Only act when the Translations workflow failed (which means a regression
# was detected — the workflow exits 1 on regression).
if: github.event.workflow_run.conclusion == 'failure'

View File

@@ -19,7 +19,7 @@ concurrency:
jobs:
frontend-check-translations:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: read
pull-requests: read
@@ -38,7 +38,7 @@ jobs:
- name: Setup Node.js
if: steps.check.outputs.frontend
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"
@@ -55,7 +55,7 @@ jobs:
npm run build-translation
babel-extract:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: read
pull-requests: read

View File

@@ -21,7 +21,7 @@ concurrency:
jobs:
app-checks:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 20
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
@@ -29,7 +29,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: './superset-websocket/.nvmrc'
- name: Install dependencies

View File

@@ -15,7 +15,7 @@ on:
jobs:
supersetbot:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@supersetbot'))

View File

@@ -32,7 +32,7 @@ concurrency:
jobs:
config:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
@@ -50,13 +50,13 @@ jobs:
needs: config
if: needs.config.outputs.has-secrets
name: docker-release
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: write
strategy:
matrix:
build_preset:
["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
["dev", "lean", "websocket", "dockerize", "py311", "py312"]
fail-fast: false
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
@@ -75,7 +75,7 @@ jobs:
- name: Use Node.js 20
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20
package-manager-cache: false
@@ -120,7 +120,7 @@ jobs:
update-prs-with-release-info:
needs: config
if: needs.config.outputs.has-secrets
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
permissions:
contents: read
pull-requests: write
@@ -133,7 +133,7 @@ jobs:
- name: Use Node.js 20
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20
package-manager-cache: false

View File

@@ -11,7 +11,7 @@ permissions:
jobs:
config:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
@@ -28,7 +28,7 @@ jobs:
process-and-upload:
needs: config
if: needs.config.outputs.has-secrets
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
name: Generate Reports
steps:
- name: Checkout Repository
@@ -37,7 +37,7 @@ jobs:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-frontend/.nvmrc"

View File

@@ -7,7 +7,7 @@ on:
jobs:
welcome:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
if: github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
permissions:
pull-requests: write

View File

@@ -15,8 +15,8 @@
# limitations under the License.
#
# Python version installed; we need 3.10-3.11
PYTHON=`command -v python3.11 || command -v python3.10`
# Python version installed; we need 3.11-3.12
PYTHON=`command -v python3.11 || command -v python3.12`
.PHONY: install superset venv pre-commit up down logs ps nuke ports open
@@ -76,7 +76,7 @@ update-js:
venv:
# Create a virtual environment and activate it (recommended)
if ! [ -x "${PYTHON}" ]; then echo "You need Python 3.10 or 3.11 installed"; exit 1; fi
if ! [ -x "${PYTHON}" ]; then echo "You need Python 3.11 or 3.12 installed"; exit 1; fi
test -d venv || ${PYTHON} -m venv venv # setup a python3 virtualenv
. venv/bin/activate

View File

@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM python:3.10-slim-trixie
FROM python:3.11-slim-trixie
RUN useradd --user-group --create-home --no-log-init --shell /bin/bash superset

View File

@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM python:3.10-slim-trixie
FROM python:3.11-slim-trixie
RUN useradd --user-group --create-home --no-log-init --shell /bin/bash superset

View File

@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM python:3.10-slim-trixie
FROM python:3.11-slim-trixie
ARG VERSION
RUN git clone --depth 1 --branch ${VERSION} https://github.com/apache/superset.git /superset

View File

@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM python:3.10-slim-trixie
FROM python:3.11-slim-trixie
RUN apt-get update -y
RUN apt-get install -y \

View File

@@ -24,6 +24,88 @@ assists people when migrating to a new version.
## Next
### Dashboard "Export Data to Excel" requires a Celery worker and S3 bucket
A new dashboard action exports every chart's data to a single multi-sheet
`.xlsx` asynchronously. It is disabled by default and turns on only when
`EXCEL_EXPORT_S3_BUCKET` is set (the endpoint returns `501` otherwise). It also
requires a running Celery worker and a configured SMTP transport, since the task
emails the requesting user a pre-signed download link. New config keys:
`EXCEL_EXPORT_S3_BUCKET`, `EXCEL_EXPORT_S3_KEY_PREFIX`,
`EXCEL_EXPORT_LINK_TTL_SECONDS`, `EXCEL_EXPORT_S3_CLIENT_KWARGS`, and
`EXCEL_EXPORT_TABLE_VIZ_TYPES`.
The feature depends on `boto3`, which is **not** installed by default; install it
with `pip install apache-superset[excel-export]`.
A second mode, **Export Images to Excel**, embeds non-table charts as rendered
images (which viz types stay tabular is controlled by
`EXCEL_EXPORT_TABLE_VIZ_TYPES`). It renders through the headless webdriver, so the
menu option only appears when the webdriver screenshot feature flags
(`ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS`,
`ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT`) are enabled.
Deployments that override `CELERY_CONFIG` must add
`"superset.tasks.export_dashboard_excel"` to their `imports` tuple, or the task
will not register and exports will silently never run.
### SQL_QUERY_MUTATOR now honors MUTATE_AFTER_SPLIT in SQL Lab
SQL Lab now applies `SQL_QUERY_MUTATOR` according to `MUTATE_AFTER_SPLIT`, matching the documented semantics and the chart/query path. This only affects deployments that define `SQL_QUERY_MUTATOR` in `superset_config.py`:
- With `MUTATE_AFTER_SPLIT = True`, the mutator previously never ran in SQL Lab; it now runs on each individual statement (including on engines like BigQuery and Kusto that execute multiple statements as one block, where each statement is mutated before the statements are joined).
- With `MUTATE_AFTER_SPLIT = False` (the default), multi-statement SQL Lab queries previously applied the mutator to each statement separately; the mutator now runs once on the whole un-split query, as documented. Single-statement queries are unaffected.
- With `MUTATE_AFTER_SPLIT = False` on engines that execute statements individually, the mutator's output is re-parsed to split it into statements. A mutator that emits SQL Superset's parser cannot parse will now fail with a clear parse error before execution, and one that strips a query down to nothing raises an invalid-SQL error instead of executing an empty query.
### Python 3.10 support removed
Python 3.10 is no longer supported. Superset now requires **Python 3.11 or higher**.
Update your environment (virtualenv, Docker base image, CI configuration, etc.) to
Python 3.11+ before upgrading. The `apache/superset-cache:3.10-slim-trixie` and
`py310` Docker image variants are no longer published.
### `from_dttm` and `to_dttm` Jinja template variables removed
The `{{ from_dttm }}` and `{{ to_dttm }}` Jinja template variables, deprecated since
v5.0, have been removed from the Jinja context and are no longer available in virtual
dataset SQL or custom SQL expressions.
Replace usages with the `get_time_filter()` function. For example:
```sql
-- Before
SELECT * FROM tbl
WHERE dttm_col > '{{ from_dttm }}' AND dttm_col < '{{ to_dttm }}'
-- After
{% set tf = get_time_filter("dttm_col") %}
SELECT * FROM tbl
WHERE dttm_col > {{ tf.from_expr }} AND dttm_col <= {{ tf.to_expr }}
```
Note that `from_expr` and `to_expr` are already fully-formatted SQL expressions (e.g.
`TO_TIMESTAMP('2024-01-01', ...)`) — do not wrap them in single quotes.
### Removed deck.gl JavaScript tooltip/data-mutator controls and ENABLE_JAVASCRIPT_CONTROLS
The `ENABLE_JAVASCRIPT_CONTROLS` feature flag and the deck.gl chart controls it gated
(`js_tooltip`, `js_onclick_href`, `js_data_mutator`, and the GeoJSON layer's label/icon
JavaScript-mode generators) have been removed. These controls let users write arbitrary
JavaScript, sandboxed via Node's `vm` module, to customize deck.gl tooltips, click
behavior, and data transforms; the flag defaulted off and the feature saw negligible use.
The deck.gl "Extra data for JS" control (`js_columns`) has also been removed. It only
ever existed to feed extra columns into the JavaScript controls above; deck.gl's
built-in field-based tooltips and cross-filtering already pull in any columns they need
via `tooltip_contents`/`cross_filter_column`, so this control had no remaining purpose.
Any chart layer whose "Advanced" control panel section only contained this control no
longer has an "Advanced" section.
Any saved charts with these fields set will simply ignore them going forward and fall back
to deck.gl's built-in field-based tooltips (`tooltip_contents`/`tooltip_template`) and
native click/cross-filter behavior. No migration is required; the fields are dropped
silently on next save.
### Owners, dashboard roles, and RLS roles replaced by Subjects
Superset now uses subject-based access assignments for dashboards, charts, datasets,
@@ -206,6 +288,22 @@ The pivot table chart's `First` and `Last` aggregations now return the first and
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`.
### Pivot Table totals are now computed by the database (per-metric "Aggregation function" control removed)
Pivot Table subtotals and grand totals are now computed by the database at each
rollup level instead of re-aggregating the already-aggregated cell values on the
client. This fixes long-standing incorrect totals for non-additive metrics
(ratios such as `SUM(a)/SUM(b)`, `COUNT_DISTINCT`, `AVG`, percentiles, etc.),
which previously summed the displayed cell values.
As a result the per-table **"Aggregation function"** control (which let you pick
how totals were aggregated client-side, e.g. Sum/Average/Count) has been
removed: totals now always reflect the metric's own definition evaluated at the
total's granularity. For additive metrics (`SUM`/`COUNT`/`MIN`/`MAX`) the result
is unchanged. Saved charts that set `aggregateFunction` will ignore it; no
migration is required. If you previously relied on a plain sum-of-cells total
for a non-additive metric, that specific behavior is no longer available.
### `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`:
@@ -216,6 +314,12 @@ The `thumbnail_url` field has been removed from `GET /api/v1/dashboard/` list re
The thumbnail endpoint redirects to the current digest URL regardless of whether the supplied digest is exact. If the image is not yet cached, that digest URL may return `202` and trigger async generation. Using `changed_on_utc` as the digest is sufficient for cache-busting purposes.
### Dashboard import can overwrite related charts, datasets, and databases
Re-importing an existing dashboard previously overwrote only the dashboard itself; its related charts, datasets, and databases were never updated (the importer hardcoded `overwrite=False` for them). They can now be overwritten as part of the import.
A new `overwrite_all` form field controls this, and defaults to `false` everywhere, so existing behavior is preserved: passing `overwrite=true` alone still overwrites only the dashboard, exactly as before. To also overwrite the related charts, datasets, and databases on the `/api/v1/dashboard/import/` endpoint, pass `overwrite_all=true` explicitly. The import modal in the UI exposes this as an "also overwrite all assets" checkbox, and the CLI `superset import-dashboards` and the `ImportDashboardsCommand` likewise default `overwrite_all` to `false`.
### Tagging fix for `create_all`-bootstrapped schemas
Only affects deployments whose metadata schema was created with SQLAlchemy's `create_all` (rather than `superset db upgrade`) on a foreign-key-enforcing backend — PostgreSQL, or MySQL with `FOREIGN_KEY_CHECKS=1`. Such schemas carry three invalid foreign keys on `tagged_object.object_id` that break tagging (`TAGGING_SYSTEM = True`) with a `ForeignKeyViolation`. Schemas built via `superset db upgrade` are unaffected.
@@ -241,6 +345,19 @@ A few save- and import-path internals change **unconditionally** (independent of
These are behavior changes that take effect on upgrade regardless of `ENABLE_VERSIONING_CAPTURE`; no operator action is required.
### Cross-entity version activity stream
A read-only companion to the version-history endpoints: each entity type gains a `GET /api/v1/{chart,dashboard,dataset}/<uuid>/activity/` endpoint returning a chronological, access-filtered stream of edits — the entity's own edits plus, for charts and dashboards, transitive edits to related entities during their association windows. Datasets have no related layer in V2, so `include=related` returns an empty stream for a dataset and `include=all` reduces to the dataset's own edits.
| Param | Type | Default | Purpose |
|---|---|---|---|
| `since` / `until` | ISO 8601 | — | Bound `issued_at` |
| `include` | `self` \| `related` \| `all` | `all` | Own edits, related edits, or both |
| `q` | string | — | Case-insensitive search over the full history, applied before pagination (so `count` reflects matches) |
| `page` / `page_size` | integer | `0` / `25` | Pagination (`page_size` clamped to 200) |
Authorization reuses the resource's `can_read` permission and per-object `raise_for_access`; related-entity rows are visibility-filtered to what the caller may see. The stream is empty unless version capture is on (`ENABLE_VERSIONING_CAPTURE`).
### Webhook alerts/reports block private/internal hosts by default
Webhook alert/report dispatch (`WebhookNotification.send`) now validates the target URL's host against the same private/internal-IP block applied to dataset import URLs. If the resolved host is in a loopback, link-local, private (RFC-1918), shared-CGNAT, or multicast range, the webhook is rejected with `NotificationParamException`.
@@ -493,6 +610,12 @@ The partial-index replacement is dialect-dependent: PostgreSQL uses a native `WH
**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, editor/viewer subjects, tags). 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 editorship of the deleted row (or admin) — non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
### Engine spec capability flag: `supports_offset`
A new `BaseEngineSpec.supports_offset` attribute (default `True`) indicates whether a database engine supports the SQL `OFFSET` clause. Engines that do not support `OFFSET` — such as Elasticsearch SQL and OpenDistro — opt out by setting it to `False`, and Superset uses each engine's cursor API to paginate drill-to-detail samples instead of emitting `OFFSET`. Downstream forks maintaining custom engine specs may set the flag to `False` (and implement `fetch_data_with_cursor`) to avoid crashes when paginated drill-to-detail queries are run against engines without `OFFSET` support.
**Note on deep-pagination cost:** Cursor-based engines (including Elasticsearch and OpenDistro) are forward-only, so reaching page `N` of a drill-to-detail view issues `N` round trips to the cluster. Deep pagination is therefore linear in page number; users paginating into the hundreds or thousands will notice added latency compared to `OFFSET`-capable engines.
### Granular Export Controls
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
@@ -529,6 +652,10 @@ Added a new combined datasource list endpoint at `GET /api/v1/datasource/` to se
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.
### Kenya Country Map: Updated Administrative Divisions
The Kenya country map has been updated to reflect the 47 counties established under Kenya's 2010 constitution, replacing the outdated 8-province boundaries from the Natural Earth dataset. County keys now use ISO 3166-2:KE codes (`KE-01` through `KE-47`), replacing the former province codes (`KE-110`, `KE-200`, ..., `KE-800`). Dashboards that join on the old province codes will need to re-key their datasets to use the new county codes.
### MCP Tool Observability
MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging:

View File

@@ -71,7 +71,7 @@ x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
cache_from:
- apache/superset-cache:3.10-slim-trixie
- apache/superset-cache:3.11-slim-trixie
args:
DEV_MODE: "true"
INCLUDE_CHROMIUM: ${INCLUDE_CHROMIUM:-false}

View File

@@ -33,7 +33,7 @@ x-common-build: &common-build
context: .
target: dev
cache_from:
- apache/superset-cache:3.10-slim-trixie
- apache/superset-cache:3.11-slim-trixie
services:
redis:

View File

@@ -38,7 +38,7 @@ x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
cache_from:
- apache/superset-cache:3.10-slim-trixie
- apache/superset-cache:3.11-slim-trixie
args:
DEV_MODE: "true"
INCLUDE_CHROMIUM: ${INCLUDE_CHROMIUM:-false}

View File

@@ -87,6 +87,7 @@ class CeleryConfig:
"superset.tasks.scheduler",
"superset.tasks.thumbnails",
"superset.tasks.cache",
"superset.tasks.export_dashboard_excel",
)
result_backend = f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_RESULTS_DB}"
worker_prefetch_multiplier = 1

View File

@@ -149,10 +149,10 @@ database engine on a separate host or container.
Superset supports the following database engines/versions:
| Database Engine | Supported Versions |
| ----------------------------------------- | ---------------------------------------- |
| [PostgreSQL](https://www.postgresql.org/) | 10.X, 11.X, 12.X, 13.X, 14.X, 15.X, 16.X |
| [MySQL](https://www.mysql.com/) | 5.7, 8.X |
| Database Engine | Supported Versions |
| ----------------------------------------- | ---------------------------------------------- |
| [PostgreSQL](https://www.postgresql.org/) | 10.X, 11.X, 12.X, 13.X, 14.X, 15.X, 16.X, 17.X |
| [MySQL](https://www.mysql.com/) | 5.7, 8.X |
Use the following database drivers and connection strings:

View File

@@ -99,7 +99,7 @@ See [Connecting AI Clients](#connecting-ai-clients) for Claude Code, Claude Web,
## Prerequisites
- Apache Superset 5.0+ running and accessible
- Python 3.10+
- Python 3.11+
- The `fastmcp` package (`pip install fastmcp`)
---

View File

@@ -39,8 +39,6 @@ made available in the Jinja context:
- `columns`: columns which to group by in the query
- `filter`: filters applied in the query
- `from_dttm`: start `datetime` value from the selected time range (`None` if undefined). **Note:** Only available in virtual datasets when a time range filter is applied in Explore/Chart views—not available in standalone SQL Lab queries. (deprecated beginning in version 5.0, use `get_time_filter` instead)
- `to_dttm`: end `datetime` value from the selected time range (`None` if undefined). **Note:** Only available in virtual datasets when a time range filter is applied in Explore/Chart views—not available in standalone SQL Lab queries. (deprecated beginning in version 5.0, use `get_time_filter` instead)
- `groupby`: columns which to group by in the query (deprecated)
- `metrics`: aggregate expressions in the query
- `row_limit`: row limit of the query
@@ -49,85 +47,26 @@ made available in the Jinja context:
- `time_column`: temporal column of the query (`None` if undefined)
- `time_grain`: selected time grain (`None` if undefined)
For example, to add a time range to a virtual dataset, you can write the following:
For example, to add a time range to a virtual dataset, use the `get_time_filter` macro:
```sql
{% set tf = get_time_filter("dttm_col") %}
SELECT *
FROM tbl
WHERE dttm_col > '{{ from_dttm }}' and dttm_col < '{{ to_dttm }}'
WHERE dttm_col > {{ tf.from_expr }} AND dttm_col <= {{ tf.to_expr }}
```
You can also use [Jinja's logic](https://jinja.palletsprojects.com/en/2.11.x/templates/#tests)
to make your query robust to clearing the timerange filter:
```sql
SELECT *
FROM tbl
WHERE (
{% if from_dttm is not none %}
dttm_col > '{{ from_dttm }}' AND
{% endif %}
{% if to_dttm is not none %}
dttm_col < '{{ to_dttm }}' AND
{% endif %}
1 = 1
)
```
The `1 = 1` at the end ensures a value is present for the `WHERE` clause even when
the time filter is not set. For many database engines, this could be replaced with `true`.
Note that the Jinja parameters are called within _double_ brackets in the query and with
_single_ brackets in the logic blocks.
### Understanding Context Availability
Some Jinja variables like `from_dttm`, `to_dttm`, and `filter` are **only available when a chart or dashboard provides them**. They are populated from:
Some Jinja variables like `filter` are **only available when a chart or dashboard provides them**. They are populated from:
- Time range filters applied in Explore/Chart views
- Dashboard native filters
- Filter components
**These variables are NOT available in standalone SQL Lab queries** because there's no filter context. If you try to use `{{ from_dttm }}` directly in SQL Lab, you'll get an "undefined parameter" error.
#### Testing Time-Filtered Queries in SQL Lab
To test queries that use time variables in SQL Lab, you have several options:
**Option 1: Use Jinja defaults (recommended)**
```sql
SELECT *
FROM tbl
WHERE dttm_col > '{{ from_dttm | default("2024-01-01", true) }}'
AND dttm_col < '{{ to_dttm | default("2024-12-31", true) }}'
```
**Option 2: Use SQL Lab Parameters**
Set parameters in the SQL Lab UI (Parameters menu):
```json
{
"from_dttm": "2024-01-01",
"to_dttm": "2024-12-31"
}
```
**Option 3: Use `{% set %}` for testing**
```sql
{% set from_dttm = "2024-01-01" %}
{% set to_dttm = "2024-12-31" %}
SELECT *
FROM tbl
WHERE dttm_col > '{{ from_dttm }}' AND dttm_col < '{{ to_dttm }}'
```
:::tip
When you save a SQL Lab query as a virtual dataset and use it in a chart with time filters,
the actual filter values will override any defaults or test values you set.
:::
To add custom functionality to the Jinja context, you need to overload the default Jinja
context in your environment by defining the `JINJA_CONTEXT_ADDONS` in your superset configuration
(`superset_config.py`). Objects referenced in this dictionary are made available for users to use
@@ -479,10 +418,13 @@ Here's a concrete example:
### Time Filter
The `{{ get_time_filter() }}` macro returns the time filter applied to a specific column. This is useful if you want
to handle time filters inside the virtual dataset, as by default the time filter is placed on the outer query. This can
considerably improve performance, as many databases and query engines are able to optimize the query better
if the temporal filter is placed on the inner query, as opposed to the outer query.
The `{{ get_time_filter() }}` macro is the standard way to reference the selected time range
in a virtual dataset. It returns a `TimeFilter` object whose `from_expr` and `to_expr`
properties are fully-formatted SQL expressions for the start and end of the range — use them
directly in your `WHERE` clause without quoting.
Placing the filter inside the virtual dataset (rather than relying on it being applied to
the outer query) also gives many database engines the opportunity to optimize the query better.
The macro takes the following parameters:

View File

@@ -379,7 +379,7 @@ functioning across environments.
Make sure your machine meets the [OS dependencies](https://superset.apache.org/docs/installation/pypi#os-dependencies) before following these steps.
You also need to install MySQL.
Ensure that you are using Python version 3.9, 3.10 or 3.11, then proceed with:
Ensure that you are using Python version 3.11 or 3.12, then proceed with:
```bash
# Create a virtual environment and activate it (recommended)

View File

@@ -82,7 +82,7 @@ If applicable, add screenshots or recordings.
### Environment
- Superset version: [e.g., 3.0.0]
- Python version: [e.g., 3.9.7]
- Python version: [e.g., 3.11.7]
- Node version: [e.g., 18.17.0]
- Database: [e.g., PostgreSQL 14]
- Browser: [e.g., Chrome 120]
@@ -125,7 +125,7 @@ No error messages in browser console or server logs.
### Environment
- Superset version: 3.0.0
- Python version: 3.9.16
- Python version: 3.11.16
- Database: PostgreSQL 14.9
- Browser: Chrome 120.0.6099.71
- OS: macOS 14.2

View File

@@ -163,10 +163,6 @@ The architecture leverages Webpack's Module Federation to enable dynamic loading
Extensions configure Webpack to expose their entry points:
```javascript
externalsType: 'window',
externals: {
'@apache-superset/core': 'superset',
},
plugins: [
new ModuleFederationPlugin({
name: 'my_extension',
@@ -178,6 +174,7 @@ plugins: [
react: { singleton: true, import: false },
'react-dom': { singleton: true, import: false },
antd: { singleton: true, import: false },
'@apache-superset/core': { singleton: true, import: false },
},
}),
]
@@ -187,9 +184,7 @@ This configuration does several important things:
**`exposes`** - Declares which modules are available to the host application. Superset always loads extensions by requesting the `./index` module from the remote container — this is a fixed convention, not a configurable value. Extensions must expose exactly `'./index': './src/index.tsx'` and place all API registrations (views, commands, menus, editors, event listeners) in that file. The module is executed as a side effect when the extension loads, so any call to `views.registerView`, `commands.registerCommand`, etc. made at the top level of `index.tsx` will run automatically.
**`externals` and `externalsType`** - Tell Webpack that when the extension imports `@apache-superset/core`, it should use `window.superset` at runtime instead of bundling its own copy. This ensures extensions use the host's implementation of shared packages.
**`shared`** - Prevents duplication of common libraries like React and Ant Design. The `singleton: true` setting ensures only one instance of each library exists, avoiding version conflicts and reducing bundle size.
**`shared`** - Prevents duplication of common libraries like React and Ant Design, and, for `@apache-superset/core`, is the mechanism that gives each extension an isolated context (see below). The `singleton: true` setting ensures only one *logical* instance of each library exists — for `react`/`react-dom`/`antd` that means the host's actual instance is reused; for `@apache-superset/core` it means the extension's container defers to whatever module the host's loader supplies for it at init time, which is not the same object for every extension (see [Runtime Resolution](#runtime-resolution)).
### Runtime Resolution
@@ -200,25 +195,30 @@ The following diagram illustrates the module loading process:
Here's what happens at runtime:
1. **Extension Registration**: When an extension is registered, Superset stores its remote entry URL
2. **Dynamic Loading**: When the extension is activated, the host fetches the remote entry file
3. **Module Resolution**: The extension imports `@apache-superset/core`, which resolves to `window.superset`
4. **Execution**: The extension code runs with access to the host's APIs and shared dependencies
2. **Dynamic Loading**: When the extension is activated, the host fetches the remote entry file, then initializes Module Federation sharing (`__webpack_init_sharing__`) and looks up the extension's container on `window`
3. **Per-Extension Scope Injection**: Before calling `container.init()`, the loader builds a **per-container copy** of the webpack share scope in which the `@apache-superset/core` entry is replaced with a synthetic module — a copy of the host's own `window.superset` implementations with `extensions.getContext` bound to that one extension's isolated context object. Because Module Federation caches the resolved module per container, every import of `@apache-superset/core` inside that extension resolves to this pre-bound copy for the lifetime of the container, and because each container gets its own independent scope object, extensions loading in parallel cannot see each other's context.
4. **Execution**: The extension code runs with access to the host's APIs (via its per-extension `@apache-superset/core` instance) and shared dependencies (`react`, `react-dom`, `antd`)
### Host API Setup
On the Superset side, the APIs are mapped to `window.superset` during application bootstrap:
On the Superset side, the real implementations backing `@apache-superset/core` (`commands`, `views`, `menus`, `extensions`, etc.) are assigned onto `window.superset` during application bootstrap, once the current user is known:
```typescript
// eslint-disable-next-line no-restricted-syntax
import * as supersetCore from '@apache-superset/core';
import { commands, views, menus, extensions /* ... */ } from 'src/core';
export default function setupExtensionsAPI() {
window.superset = {
...supersetCore,
};
}
window.superset = {
...supersetCore,
commands,
views,
menus,
extensions,
// ...
};
```
This function runs before any extensions are loaded, ensuring the APIs are available when extensions import from `@apache-superset/core`.
This runs before any extensions are loaded. `window.superset` is not itself what an extension's `@apache-superset/core` import resolves to — it is the source object the loader reads from when building each extension's per-container scoped instance (see [Runtime Resolution](#runtime-resolution)), so every extension ends up sharing the same underlying `commands`/`views`/`menus` singletons but with its own `extensions.getContext()`.
### Benefits

View File

@@ -47,13 +47,14 @@ Extension developers have access to pre-built UI components via `@apache-superse
## Next Steps
- **[Quick Start](./quick-start.md)** - Build your first extension with a complete walkthrough
- **[Architecture](./architecture.md)** - Design principles and system overview
- **[Dependencies](./dependencies.md)** - Managing dependencies and understanding API stability
- **[Contribution Types](./contribution-types.md)** - Available extension points
- **[Development](./development.md)** - Project structure, APIs, and development workflow
- **[Deployment](./deployment.md)** - Packaging and deploying extensions
- **[MCP Integration](./mcp.md)** - Adding AI agent capabilities using extensions
- **[Security](./security.md)** - Security considerations and best practices
- **[Tasks](./tasks.md)** - Framework for creating and managing long running tasks
- **[Community Extensions](./registry.md)** - Browse extensions shared by the community
- **[Quick Start](./quick-start)** - Build your first extension with a complete walkthrough
- **[Architecture](./architecture)** - Design principles and system overview
- **[Dependencies](./dependencies)** - Managing dependencies and understanding API stability
- **[Contribution Types](./contribution-types)** - Available extension points
- **[Development](./development)** - Project structure, APIs, and development workflow
- **[Deployment](./deployment)** - Packaging and deploying extensions
- **[MCP Integration](./mcp)** - Adding AI agent capabilities using extensions
- **[Security](./security)** - Security considerations and best practices
- **[Storage](./storage)** - Managed storage API for persisting extension data
- **[Tasks](./tasks)** - Framework for creating and managing long running tasks
- **[Community Extensions](./registry)** - Browse extensions shared by the community

View File

@@ -223,7 +223,7 @@ The `@apache-superset/core` package must be listed in both `peerDependencies` (t
**`frontend/webpack.config.js`**
The webpack configuration requires specific settings for Module Federation. Key settings include `externalsType: "window"` and `externals` to map `@apache-superset/core` to `window.superset` at runtime, `import: false` for shared modules to use the host's React instead of bundling a separate copy, and `remoteEntry.[contenthash].js` for cache busting.
The webpack configuration requires specific settings for Module Federation. `@apache-superset/core` is declared as a `shared` singleton alongside `react`/`react-dom`/`antd` — Superset injects a per-extension instance of the package into the container's share scope at load time, so the extension's own webpack config only needs to mark it `shared` with `singleton: true`, not resolve it itself. `import: false` on each shared entry means the extension doesn't bundle its own copy and instead uses the instance the host (or, for `@apache-superset/core`, the loader) provides. `remoteEntry.[contenthash].js` gives the built remote entry file a content hash for cache busting.
**Convention**: Superset always loads extensions by requesting the `./index` module from the Module Federation container. The `exposes` entry must be exactly `'./index': './src/index.tsx'` — do not rename or add additional entries. All API registrations must be reachable from that file. See [Architecture](./architecture.md#module-federation) for a full explanation.
@@ -255,11 +255,6 @@ module.exports = (env, argv) => {
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
// Map @apache-superset/core imports to window.superset at runtime
externalsType: 'window',
externals: {
'@apache-superset/core': 'superset',
},
module: {
rules: [
{
@@ -292,6 +287,10 @@ module.exports = (env, argv) => {
requiredVersion: packageConfig.peerDependencies['antd'],
import: false,
},
'@apache-superset/core': {
singleton: true,
import: false, // Resolved to a per-extension instance by the loader
},
},
}),
],
@@ -498,7 +497,7 @@ Here's what happens when your extension loads:
1. **Superset starts**: Reads `manifest.json` from the `.supx` bundle and loads the backend entrypoint
2. **Backend registration**: `entrypoint.py` imports your API class, triggering the [`@api`](https://github.com/apache/superset/blob/master/superset-core/src/superset_core/rest_api/decorators.py) decorator to register it automatically
3. **Frontend loads**: When SQL Lab opens, Superset fetches the remote entry file
4. **Module Federation**: Webpack loads your extension module and resolves `@apache-superset/core` to `window.superset`
4. **Module Federation**: Webpack loads your extension module; the extension's `@apache-superset/core` import resolves to a per-extension instance injected into its container's share scope by the loader
5. **Registration**: The module executes at load time, calling `views.registerView` to register your panel
6. **Rendering**: When the user opens your panel, React renders `<HelloWorldPanel />`
7. **API call**: The component fetches data from `/extensions/my-org/hello-world/message`

View File

@@ -0,0 +1,465 @@
---
title: Storage
sidebar_position: 8
---
<!--
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.
-->
# Storage
Superset Extensions have access to a managed storage API for persisting data. The storage system provides multiple tiers with different persistence characteristics, allowing extensions to choose the right storage for their needs.
Each extension receives its own isolated storage namespace. When Superset loads your extension, it binds storage to your extension's unique identifier, ensuring data privacy—two extensions using the same key will never collide, and extensions cannot access each other's data.
Storage is always accessed through the extension context via `getContext()`. This binding to the context is what ties every operation to the current extension's namespace.
## Storage Tiers
| Tier | Storage Type | Context Property | Use Case |
| ---- | ----------------- | ------------------------------------------ | -------------------------------------- |
| 1 | Browser storage | `ctx.storage.local`, `ctx.storage.session` | UI state, wizard progress, draft forms |
| 2 | Server-side cache | `ctx.storage.ephemeral` | Job progress, temporary results |
| 3 | Database | `ctx.storage.persistent` | Saved artifacts, durable config |
## Tier 1: Local State
Browser-based storage that persists on the user's device. Use this for UI state and settings that don't need to sync across devices.
### Why Use the API Instead of localStorage Directly?
You might wonder why extensions should use `ctx.storage.local` instead of directly accessing `window.localStorage`. The managed API provides several benefits:
- **Automatic namespacing**: Each extension's data is isolated. Two extensions using the same key name won't collide.
- **User isolation**: By default, data is scoped to the current user, preventing data leakage between users on shared devices.
- **Clean uninstall**: When an extension is uninstalled, all its data can be cleanly removed using prefix-based deletion.
- **Future sandboxing**: The async API is designed for a future sandboxed execution model where extensions run in isolated contexts without direct DOM access.
- **Consistent patterns**: The same API shape works across all storage tiers, making it easy to switch between them.
### localState
Data persists across browser sessions until explicitly deleted or the user clears browser storage.
```typescript
import { extensions } from '@apache-superset/core';
const ctx = extensions.getContext();
// Save sidebar state
await ctx.storage.local.set('sidebar_collapsed', true);
// Retrieve it later
const isCollapsed = await ctx.storage.local.get('sidebar_collapsed');
// Remove it
await ctx.storage.local.remove('sidebar_collapsed');
```
### sessionState
Data is cleared when the browser tab is closed. Use for transient state within a single session.
```typescript
import { extensions } from '@apache-superset/core';
const ctx = extensions.getContext();
// Save wizard progress (lost when tab closes)
await ctx.storage.session.set('wizard_step', 3);
await ctx.storage.session.set('unsaved_form', { name: 'Draft' });
// Retrieve on page reload within same tab
const step = await ctx.storage.session.get('wizard_step');
```
Tier 1 has no `shared` accessor. Browser storage is per-device, so a "shared" value would only be visible to other users of that same browser, not to other users of the extension generally — see `.shared` on [Tier 2](#tier-2-ephemeral-state) or [Tier 3](#tier-3-persistent-state) for storage that's actually shared across users.
### When to Use Tier 1
- UI state (sidebar collapsed, panel sizes)
- Recently used items
- Draft form values
- Any data acceptable to lose if user clears browser
### Limitations
- Per-browser, per-device (not shared across devices)
- Subject to browser storage quotas (~5-10 MB)
- Not accessible from backend code
## Tier 2: Ephemeral State
Server-side cache storage with automatic TTL expiration. Use for temporary data that needs to be shared between frontend and backend, or persist across page reloads.
TTL is required for every `set` call. The maximum allowed TTL is controlled by `MAX_TTL` in the server configuration.
### Frontend Usage
```typescript
import { extensions } from '@apache-superset/core';
const ctx = extensions.getContext();
// Store with a 5-minute TTL
await ctx.storage.ephemeral.set('job_progress', { pct: 42, status: 'running' }, { ttl: 300 });
// Retrieve
const progress = await ctx.storage.ephemeral.get('job_progress');
// Remove
await ctx.storage.ephemeral.remove('job_progress');
```
### Backend Usage
```python
from superset_core.extensions.context import get_context
from superset_core.extensions.storage.ephemeral import EphemeralSetOptions
ctx = get_context()
# Store job progress with a 5-minute TTL
ctx.storage.ephemeral.set(
'job_progress', {'pct': 42, 'status': 'running'}, EphemeralSetOptions(ttl=300)
)
# Retrieve
progress = ctx.storage.ephemeral.get('job_progress')
# Remove
ctx.storage.ephemeral.remove('job_progress')
```
### Shared State
For data that needs to be visible to all users:
```typescript
import { extensions } from '@apache-superset/core';
const ctx = extensions.getContext();
await ctx.storage.ephemeral.shared.set('shared_result', { data: [1, 2, 3] }, { ttl: 3600 });
const result = await ctx.storage.ephemeral.shared.get('shared_result');
```
```python
from superset_core.extensions.context import get_context
from superset_core.extensions.storage.ephemeral import EphemeralSetOptions
ctx = get_context()
ctx.storage.ephemeral.shared.set(
'shared_result', {'data': [1, 2, 3]}, EphemeralSetOptions(ttl=3600)
)
result = ctx.storage.ephemeral.shared.get('shared_result')
```
### When to Use Tier 2
- Background job progress indicators
- Cross-request intermediate state
- Query result previews
- Temporary computation results
- Any data that can be recomputed if lost
### Limitations
- Not guaranteed to survive server restarts
- Subject to cache eviction under memory pressure
- TTL-based expiration (data disappears after timeout)
- TTL is required on every `set` call
## Tier 3: Persistent State
Database-backed storage that survives server restarts, cache evictions, and browser clears. Use for any data that must not be lost.
### Frontend Usage
```typescript
import { extensions } from '@apache-superset/core';
const ctx = extensions.getContext();
// Store a saved SQL snippet
await ctx.storage.persistent.set('snippet:top_customers', { sql: 'SELECT ...' });
// Retrieve
const snippet = await ctx.storage.persistent.get('snippet:top_customers');
// Remove
await ctx.storage.persistent.remove('snippet:top_customers');
```
### Backend Usage
```python
from superset_core.extensions.context import get_context
ctx = get_context()
# Store a saved SQL snippet
ctx.storage.persistent.set('snippet:top_customers', {'sql': 'SELECT ...'})
# Retrieve
snippet = ctx.storage.persistent.get('snippet:top_customers')
# Remove
ctx.storage.persistent.remove('snippet:top_customers')
```
### Shared State
For data that should be visible to all users of the extension:
```typescript
import { extensions } from '@apache-superset/core';
const ctx = extensions.getContext();
await ctx.storage.persistent.shared.set('global_config', { version: 2 });
const config = await ctx.storage.persistent.shared.get('global_config');
```
```python
from superset_core.extensions.context import get_context
ctx = get_context()
ctx.storage.persistent.shared.set('global_config', {'version': 2})
config = ctx.storage.persistent.shared.get('global_config')
```
### Listing Entries
Both the frontend and backend accessors support listing entries in the caller's scope, without needing to know every key in advance. `page` and `pageSize`/`page_size` are required on every call — `list()` always returns one page of results, never the whole result set, so there's no default that could make that fact easy to miss. Check the returned `count` (total entries matching the scope/filters, across all pages) against `pageSize`/`page_size` to know whether more pages exist.
```typescript
import { extensions } from '@apache-superset/core';
const ctx = extensions.getContext();
// Filter by resource
const result = await ctx.storage.persistent.list({
resourceType: 'dashboard',
resourceUuid: '1234-uuid',
page: 0,
pageSize: 25,
});
result.entries.forEach(entry => {
console.log(entry.key, entry.value, entry.codec);
});
console.log(result.count); // total matching entries across all pages
// Shared (global) scope
const shared = await ctx.storage.persistent.shared.list({ page: 0, pageSize: 10 });
```
```python
from superset_core.extensions.context import get_context
from superset_core.extensions.storage.persistent import PersistentListOptions
ctx = get_context()
# Filter by resource
result = ctx.storage.persistent.list(
PersistentListOptions(
resource_type="dashboard",
resource_uuid="1234-uuid",
page=0,
page_size=25,
)
)
for entry in result.entries:
print(entry.key, entry.value, entry.codec)
print(result.count) # total matching entries across all pages
# Shared (global) scope
shared = ctx.storage.persistent.shared.list(PersistentListOptions(page=0, page_size=10))
```
`resource_type`/`resource_uuid` filter to entries linked to one resource — since a single resource can have several keyed entries (e.g. `"layout"`, `"notes"`), this is how an extension enumerates all of them without tracking key names itself.
There is no fixed limit on `page_size`, but the combined size of a requested page's values is checked against `MAX_LIST_PAYLOAD_SIZE` (see [Quotas](#quotas)) before any row's value is read from the database — a page that would exceed it is rejected rather than silently truncated, so reduce `page_size` and retry if that happens.
Backend `list()` decodes every entry's value unconditionally. The REST API backing the frontend's `list()` call is more restrictive: an entry written with a codec that isn't safe to decode over the wire (e.g. `pickle`) comes back with `value: null` (its `codec` is still reported) rather than being decoded.
### Enumerating and Managing Storage from the Backend
`ctx.storage.persistent.list()` covers the common case of listing an extension's own entries. Backend extension code that needs lower-level bulk operations — for example, a cleanup routine that removes storage linked to resources that no longer exist, using filters `list()` doesn't expose — can use `superset_core.extensions.storage.dao.ExtensionStorageDAO` instead:
```python
from superset_core.extensions.storage.dao import ExtensionStorageDAO
# All entries this extension has linked to a given resource type
entries = ExtensionStorageDAO.filter_by(resource_type="my-resource-type")
# Bulk-delete rows for resources that no longer exist
orphaned = [e for e in entries if e.resource_uuid not in my_extension_live_resource_ids()]
ExtensionStorageDAO.delete(orphaned)
```
`ExtensionStorageDAO` is automatically scoped to the calling extension's own rows — `extension_id` is never a parameter you pass, so there is no way to reach another extension's storage through this API. It supports the standard DAO operations `find_all`, `find_one_or_none`, `filter_by`, `query`, `update`, and `delete`. `create()` is not supported — it's a raw insert with no upsert dedup, quota check, or locking against the key `.set()` writes to, so use `ctx.storage.persistent.set()` to write a value instead.
### When to Use Tier 3
- Extension configuration that must survive restarts
- User-specific saved artifacts that need to roam across devices and browsers (e.g. saved SQL snippets, annotations)
- Any data where loss is unacceptable
### Limitations
- Higher latency than Tiers 12 (database round-trip per operation)
- Subject to the per-value size limit configured via `EXTENSIONS_PERSISTENT_STORAGE.MAX_VALUE_SIZE` (see [Quotas](#quotas))
- Subject to the per-extension quota configured via `EXTENSIONS_PERSISTENT_STORAGE.QUOTA_PER_EXTENSION` (see [Quotas](#quotas))
- Requires a database migration when first deployed
## Working with Binary Data
Tiers 2 and 3 accept a `codec` option describing how `value` is encoded — `"json"` (the default) for JSON-native values, or `"binary"` for raw bytes. `list()` and `get()` report which codec an entry was written with via `entry.codec`.
### Backend
Backend code talks to the storage DAO directly and works with real Python `bytes` — no extra flag is needed:
```python
from superset_core.extensions.context import get_context
from superset_core.extensions.storage.persistent import PersistentSetOptions
ctx = get_context()
with open('logo.png', 'rb') as f:
png_bytes = f.read()
ctx.storage.persistent.set('logo', png_bytes, PersistentSetOptions(codec='binary'))
stored = ctx.storage.persistent.get('logo') # raw bytes back, unchanged
```
### Frontend
The frontend SDK talks to the backend over a JSON REST API, which has no byte type — a binary value must be base64-encoded to travel in a JSON request body. There is no way for the SDK to infer this from a JS value's type, so it is never detected automatically: set `isBinary: true` explicitly whenever `value` is binary.
```typescript
import { extensions } from '@apache-superset/core';
const ctx = extensions.getContext();
const pngBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47 /* ... */]);
await ctx.storage.persistent.set('logo', pngBytes, { isBinary: true });
```
`get()` returns a binary entry's value exactly as stored on the wire — a base64 string, not a `Uint8Array` — so decode it yourself:
```typescript
const base64 = await ctx.storage.persistent.get<string>('logo');
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
```
`list()` entries report `isBinary` alongside `codec`, so you know which entries need decoding before you read `value`:
```typescript
const result = await ctx.storage.persistent.list({ page: 0, pageSize: 10 });
result.entries.forEach(entry => {
const value = entry.isBinary
? Uint8Array.from(atob(entry.value as string), c => c.charCodeAt(0))
: entry.value;
});
```
## Key Patterns
All storage keys are automatically namespaced:
| Scope | Key Pattern |
| ---------------------- | -------------------------------------------------- |
| User-scoped | `superset-ext:{extension_id}:user:{user_id}:{key}` |
| Shared (Tiers 2 and 3) | `superset-ext:{extension_id}:shared:{key}` |
This ensures:
- Extensions cannot accidentally access each other's data
- Users cannot see other users' data (by default)
- Clean prefix-based deletion on uninstall
## Configuration
### Tier 2: Ephemeral Storage
Administrators can configure the server-side cache backend in `superset_config.py`:
```python
EXTENSIONS_EPHEMERAL_STORAGE = {
# Use Redis for better performance in production
"CACHE_TYPE": "RedisCache",
"CACHE_REDIS_URL": "redis://localhost:6379/2",
"MAX_TTL": 604800, # 7 days maximum TTL
"MAX_VALUE_SIZE": 100 * 1024, # 100 KB maximum per stored value
}
```
For development, the default `SupersetMetastoreCache` stores data in the metadata database.
### Tier 3: Persistent Storage
Tier 3 values are stored in the `extension_storage` database table. Values are stored unencrypted by default; encryption is opt-in per write, available via a `PersistentSetOptions`-shaped `encrypt` option:
```typescript
import { extensions } from '@apache-superset/core';
const ctx = extensions.getContext();
await ctx.storage.persistent.set('api_token', 'sk-...', { encrypt: true });
const token = await ctx.storage.persistent.get('api_token');
```
```python
from superset_core.extensions.context import get_context
from superset_core.extensions.storage.persistent import PersistentSetOptions
ctx = get_context()
ctx.storage.persistent.set('api_token', 'sk-...', PersistentSetOptions(encrypt=True))
token = ctx.storage.persistent.get('api_token')
```
Encryption reuses Superset's existing `EncryptedType` (from `sqlalchemy-utils`) rather than a separate mechanism — the same infrastructure used for database connection credentials. The key is not configured directly: user-scoped values are encrypted with a key derived per-user via HMAC-SHA256 from the deployment's `SECRET_KEY`, so ciphertext for one user cannot be decrypted as another's; shared/global values (written via `.shared`) use `SECRET_KEY` directly. The encryption engine (AES-CBC by default) can be changed for the whole deployment via the existing `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE` config, the same setting that controls encryption for database credentials elsewhere in Superset — there is no separate key list or rotation mechanism specific to extension storage. When `SECRET_KEY` is rotated, extension storage rows are re-encrypted by the existing `SecretsMigrator` tooling alongside database credentials, with no extension-specific steps required.
### Quotas
Persistent storage is subject to per-value, per-list-page, and per-extension size limits, configured in `superset_config.py`:
```python
EXTENSIONS_PERSISTENT_STORAGE = {
# Maximum total bytes an extension may store across all of its rows
# (global, every user's, and shared). Defaults to 100 MB.
"QUOTA_PER_EXTENSION": 100 * 1024 * 1024,
# Maximum size (in bytes) of a single stored value. Defaults to 1 MB.
"MAX_VALUE_SIZE": 1024 * 1024,
# Maximum combined value size (in bytes) that a single list() page may
# return. Defaults to 10 MB.
"MAX_LIST_PAYLOAD_SIZE": 10 * 1024 * 1024,
}
```
A write that would push the extension's total stored size over `QUOTA_PER_EXTENSION` is rejected — the frontend SDK's `set()` call rejects with an HTTP 413 response, and backend code calling `ctx.storage.persistent.set()` sees the corresponding exception raised. Overwriting an existing key nets out that key's own current size against usage first, so replacing a value with one of the same or smaller size never spuriously fails even when the extension is already near its quota.
A write whose encoded value exceeds `MAX_VALUE_SIZE` is rejected outright, independently of the total quota.
A `list()` call whose requested page would return more than `MAX_LIST_PAYLOAD_SIZE` combined bytes of value data is rejected — reduce `page`/`pageSize` and retry. This check runs against the page's stored sizes before any value is read from the database, so an oversized request fails fast. Because `list()`'s response is consumed as JSON in the browser, raising `MAX_LIST_PAYLOAD_SIZE` substantially above the default risks client-side memory and parse-time issues — treat the default as already generous rather than a floor to build up from.

View File

@@ -52,7 +52,7 @@ Everything you need to contribute to the Apache Superset project. This section i
## Development Resources
### Prerequisites
- **Python**: 3.9, 3.10, or 3.11
- **Python**: 3.11 or 3.12
- **Node.js**: 18.x or 20.x
- **npm**: 9.x or 10.x
- **Git**: Basic understanding

View File

@@ -0,0 +1,99 @@
---
title: Exporting Dashboard Data to Excel
hide_title: true
sidebar_position: 7
version: 1
---
# Exporting Dashboard Data to Excel
Superset can export every chart on a dashboard to a single Excel workbook, with
each chart's underlying data rendered as its own worksheet. The export reflects
the dashboard's currently applied filters and runs asynchronously: when it
finishes, the requesting user receives an email with a time-limited download
link.
## Using the export
From a dashboard, open the **... (actions) → Download** submenu and choose
**Export Data to Excel**. The action appears for users who have the dashboard
`can_export` permission. You'll see a confirmation that the export is being
prepared; the workbook arrives by email when it's ready.
A second option, **Export Images to Excel**, embeds each non-table chart as a
rendered image (tables stay tabular) instead of exporting raw data. Because it
renders charts through the headless webdriver, this option only appears when the
webdriver screenshot feature flags are enabled (see the prerequisites below);
which viz types stay tabular is controlled by `EXCEL_EXPORT_TABLE_VIZ_TYPES`.
Notes on the generated workbook:
- One worksheet per chart, named `{chart_id} - {chart title}` (truncated to
Excel's 31-character limit; the chart id keeps names unique).
- Charts nested in tabs are included.
- Data reflects the dashboard's active filter state at the time of export.
- A chart with no saved query context is skipped and listed in the email; open
the chart in Explore and re-save it to include it next time.
- Row counts per sheet are capped the same way as the chart-level CSV/Excel
export (`ROW_LIMIT`, bounded by `SQL_MAX_ROW`), and never exceed Excel's
per-sheet maximum.
## Prerequisites
This feature is **disabled by default**. It requires:
1. **The `boto3` dependency.** It is not installed by default; install it with
`pip install apache-superset[excel-export]`. Without it, exports fail and the
user receives a failure email.
2. **An S3 bucket.** Set `EXCEL_EXPORT_S3_BUCKET`. Until it is set, the export
endpoint returns `501` and the menu action surfaces a "not configured"
message.
3. **A running Celery worker.** The export runs as a Celery task. If no worker
is running, the request is accepted but nothing is produced.
4. **A configured SMTP transport.** The download link is delivered by email
using the same settings as alerts & reports (`SMTP_*`,
`EMAIL_REPORTS_SUBJECT_PREFIX`).
**Export Images to Excel** additionally requires a working headless webdriver —
the same infrastructure scheduled reports and thumbnails use (`WEBDRIVER_*`,
plus the `ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS` and
`ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT` feature flags). The menu option
is hidden when those flags are off; if the webdriver is unreachable, image
charts come back empty even though the data export path still works.
Deployments that override `CELERY_CONFIG` must add
`"superset.tasks.export_dashboard_excel"` to the `imports` tuple, or the task
will not register.
## Configuration keys
| Key | Default | Description |
| --- | --- | --- |
| `EXCEL_EXPORT_S3_BUCKET` | `None` | Destination bucket. Required; `501` if unset. |
| `EXCEL_EXPORT_S3_KEY_PREFIX` | `"dashboard-exports/"` | Key prefix: `{prefix}{dashboard_id}/{job_id}.xlsx`. |
| `EXCEL_EXPORT_LINK_TTL_SECONDS` | `86400` | Lifetime of the pre-signed download URL (24h). |
| `EXCEL_EXPORT_S3_CLIENT_KWARGS` | `{}` | Extra kwargs for `boto3.client("s3", ...)` — e.g. `region_name`, or `endpoint_url` for MinIO/LocalStack. |
| `EXCEL_EXPORT_TABLE_VIZ_TYPES` | `None` | Viz types kept tabular in **Export Images to Excel** mode; every other type is embedded as an image. `None` uses the built-in default (`table`, `pivot_table`, `pivot_table_v2`). |
Credentials and region resolve through the standard boto3 chain (environment
variables, shared config, or instance role) unless overridden via
`EXCEL_EXPORT_S3_CLIENT_KWARGS`. The worker needs `s3:PutObject` on the bucket.
## Security considerations
- The emailed link is a **pre-signed S3 URL**: anyone who holds it can download
the workbook until it expires. Keep the bucket **private**, enable
encryption, and consider a lifecycle rule to delete objects after a few days.
Lower `EXCEL_EXPORT_LINK_TTL_SECONDS` if 24 hours is too long for your data.
- The export runs with the requesting user's permissions; each chart's query is
access-checked, so users only ever receive data they are entitled to.
## Limitations
- **Embedded dashboards / guest tokens are not supported** in this version,
because guest users have no email address to deliver the link to. Logged-in
users viewing an embedded dashboard can still use the export.
- The default **Export Data to Excel** mode exports data only (no visual
styling). Use **Export Images to Excel** to embed rendered chart images, which
requires the webdriver infrastructure described in the prerequisites.
- Scheduled/automated exports are not part of this feature.

View File

@@ -146,7 +146,9 @@ The `where_in` filter converts the list to SQL format: `('value1', 'value2', 'va
### Time Filters
For charts with time range filters:
`get_time_filter()` is the standard way to reference the selected time range in a virtual dataset.
It returns a `TimeFilter` object with `from_expr` and `to_expr` — fully-formatted SQL expressions
that should be used directly in `WHERE` clauses without quoting.
| Macro | Description |
|-------|-------------|
@@ -198,17 +200,9 @@ GROUP BY category
## Testing Templates in SQL Lab
Some variables like `from_dttm` and `filter_values()` only work when filters are applied from dashboards or charts. To test in SQL Lab:
Some variables like `filter_values()` only work when filters are applied from dashboards or charts. To test in SQL Lab:
**Option 1: Use defaults**
```sql
SELECT *
FROM orders
WHERE date >= '{{ from_dttm | default("2024-01-01", true) }}'
```
**Option 2: Set test parameters**
**Option 1: Set test parameters**
Add to the Parameters menu:
@@ -220,7 +214,7 @@ Add to the Parameters menu:
}
```
**Option 3: Use `{% set %}`**
**Option 2: Use `{% set %}`**
```sql
{% set start_date = "2024-01-01" %}

View File

@@ -44,12 +44,12 @@
},
"dependencies": {
"@ant-design/icons": "^6.2.5",
"@docusaurus/core": "^3.10.1",
"@docusaurus/faster": "^3.10.1",
"@docusaurus/plugin-client-redirects": "^3.10.1",
"@docusaurus/preset-classic": "3.10.1",
"@docusaurus/theme-live-codeblock": "^3.10.1",
"@docusaurus/theme-mermaid": "^3.10.1",
"@docusaurus/core": "^3.10.2",
"@docusaurus/faster": "^3.10.2",
"@docusaurus/plugin-client-redirects": "^3.10.2",
"@docusaurus/preset-classic": "3.10.2",
"@docusaurus/theme-live-codeblock": "^3.10.2",
"@docusaurus/theme-mermaid": "^3.10.2",
"@emotion/core": "^11.0.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.14.1",
@@ -58,14 +58,14 @@
"@fontsource/inter": "^5.2.8",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@storybook/addon-docs": "^10.4.5",
"@storybook/addon-docs": "^10.5.0",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.43",
"antd": "^6.5.0",
"baseline-browser-mapping": "^2.10.40",
"caniuse-lite": "^1.0.30001803",
"docusaurus-plugin-openapi-docs": "^5.1.1",
"docusaurus-theme-openapi-docs": "^5.1.1",
"antd": "^6.5.1",
"baseline-browser-mapping": "^2.10.43",
"caniuse-lite": "^1.0.30001805",
"docusaurus-plugin-openapi-docs": "^5.1.2",
"docusaurus-theme-openapi-docs": "^5.1.2",
"js-yaml": "^5.2.0",
"json-bigint": "^1.0.0",
"prism-react-renderer": "^2.4.1",
@@ -77,20 +77,20 @@
"react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.2.0",
"storybook": "^10.4.5",
"storybook": "^10.5.0",
"swagger-ui-react": "^5.32.8",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",
"unist-util-visit": "^5.1.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.10.1",
"@docusaurus/tsconfig": "^3.10.1",
"@docusaurus/module-type-aliases": "^3.10.2",
"@docusaurus/tsconfig": "^3.10.2",
"@eslint/js": "^9.39.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.63.0",
"@typescript-eslint/eslint-plugin": "^8.64.0",
"@typescript-eslint/parser": "^8.64.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6",
@@ -98,7 +98,7 @@
"globals": "^17.7.0",
"prettier": "^3.9.5",
"typescript": "~6.0.3",
"typescript-eslint": "^8.63.0",
"typescript-eslint": "^8.64.0",
"webpack": "^5.108.2"
},
"browserslist": {
@@ -124,8 +124,6 @@
"serialize-javascript": "7.0.5",
"d3-color": "3.1.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"

View File

@@ -82,12 +82,14 @@ const sidebars = {
items: [
'extensions/extension-points/sqllab',
'extensions/extension-points/editors',
'extensions/extension-points/chat',
],
},
'extensions/development',
'extensions/deployment',
'extensions/mcp',
'extensions/security',
'extensions/storage',
'extensions/tasks',
'extensions/registry',
],

View File

@@ -28,8 +28,9 @@ RewriteRule ^(.*)$ https://superset.apache.org/$1 [R=301,L]
# - *.googleapis.com, *.google.com, *.gstatic.com: Google Calendar embed, kapa.ai reCAPTCHA - all of these loaded with user consent, following policy laid out in https://privacy.apache.org/faq/committers.html
# - github.com, *.github.com, *.githubusercontent.com: GitHub user-attachment images in docs (apex github.com serves user-attachments/* assets). Discussed/resolved in this thread: https://issues.apache.org/jira/browse/INFRA-25701?filter=-2 (DPA in place with GitHub)
# - *.algolia.net, *.algolianet.com: Algolia DocSearch. Approved here: https://privacy.apache.org/faq/committers.html
# - *.run.app: Google-owned domain (Google Cloud Run) used by the kapa.ai widget backend - only reached after explicit user acceptance when opening the widget, allowable per the policy laid out in https://privacy.apache.org/faq/committers.html
# See: https://infra.apache.org/tools/csp.html
SetEnv CSP_PROJECT_DOMAINS "widget.kapa.ai https://*.googleapis.com/ https://*.google.com/ https://*.gstatic.com/ https://github.com/ https://*.github.com/ https://*.githubusercontent.com/ https://*.algolia.net/ https://*.algolianet.com/"
SetEnv CSP_PROJECT_DOMAINS "widget.kapa.ai https://*.googleapis.com/ https://*.google.com/ https://*.gstatic.com/ https://github.com/ https://*.github.com/ https://*.githubusercontent.com/ https://*.algolia.net/ https://*.algolianet.com/ https://*.run.app/"
# REDIRECTS

View File

@@ -405,12 +405,6 @@
"default": true,
"lifecycle": "deprecated",
"description": "Enable drill-to-detail functionality in charts"
},
{
"name": "ENABLE_JAVASCRIPT_CONTROLS",
"default": false,
"lifecycle": "deprecated",
"description": "Allow JavaScript in chart controls. WARNING: XSS security vulnerability!"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.21.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.22.1 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.21.0](https://img.shields.io/badge/Version-0.21.0-informational?style=flat-square)
![Version: 0.22.1](https://img.shields.io/badge/Version-0.22.1-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -125,6 +125,13 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| fullnameOverride | string | `nil` | Provide a name to override the full names of resources |
| globalPodAnnotations | object | `{}` | Global pod annotations to be added to all pods Use this to set annotations that apply to all Superset components Component-specific podAnnotations will be merged with these global annotations |
| hostAliases | list | `[]` | Custom hostAliases for all superset pods # https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/ |
| httproute | object | see `values.yaml` | Gateway API HTTPRoute for exposing Superset via a Gateway. Requires the Gateway API CRDs (gateway.networking.k8s.io/v1) installed in the cluster. |
| httproute.annotations | object | `{}` | Annotations to add to the HTTPRoute |
| httproute.apiVersion | string | `"gateway.networking.k8s.io/v1"` | HTTPRoute apiVersion. Override to gateway.networking.k8s.io/v1beta1 for older Gateway API installations that have not promoted HTTPRoute to v1. |
| httproute.hostnames | list | `[]` | Hostnames that match against the HTTP Host header (templated) |
| httproute.labels | object | `{}` | Additional labels to add to the HTTPRoute |
| httproute.parentRefs | list | `[]` | Gateways this HTTPRoute attaches to |
| httproute.rules | list | `[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | Routing rules. Each rule is backed by the Superset service. Set `weight` per rule to leave room for traffic splitting (defaults to 1). When `supersetWebsockets.enabled` is true, an extra rule routing `supersetWebsockets.ingress.path` to the `-ws` service is appended automatically, mirroring the ingress behavior. |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"apachesuperset.docker.scarf.sh/apache/superset"` | |
| image.tag | string | `nil` | |
@@ -169,7 +176,10 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| redis | object | see `values.yaml` | Configuration values for the Redis dependency. ref: https://github.com/bitnami/charts/blob/master/bitnami/redis More documentation can be found here: https://artifacthub.io/packages/helm/bitnami/redis |
| resources | object | `{}` | |
| runAsUser | int | `0` | User ID directive. This user must have enough permissions to run the bootstrap script Running containers as root is not recommended in production. Change this to another UID - e.g. 1000 to be more secure |
| secretEnv | object | `{"create":true}` | Specify rather or not helm should create the secret described in `secret-env.yaml` template |
| secretConfig | object | `{"annotations":{}}` | Specify whether Helm should apply additional settings to a secret containing configuration files |
| secretConfig.annotations | object | `{}` | Annotations to be added to the secret which contains configuration files |
| secretEnv | object | `{"annotations":{},"create":true}` | Specify rather or not helm should create the secret described in `secret-env.yaml` template |
| secretEnv.annotations | object | `{}` | Annotations to be added to the secret |
| secretEnv.create | bool | `true` | Change to false in order to support externally created secret (Binami "Sealed Secrets" for Kubernetes or External Secrets Operator) note: when externally creating the secret, the chart still expects to pull values from a secret with the name of the release defaults to `release-name-superset-env` - full logic located in _helpers.tpl file: `define "superset.fullname"` |
| service.annotations | object | `{}` | |
| service.loadBalancerIP | string | `nil` | |
@@ -246,6 +256,60 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetCeleryFlower.startupProbe.successThreshold | int | `1` | |
| supersetCeleryFlower.startupProbe.timeoutSeconds | int | `1` | |
| supersetCeleryFlower.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetCeleryFlower deployments |
| supersetMcp.affinity | object | `{}` | Affinity to be added to supersetMcp deployment |
| supersetMcp.command | list | a `superset mcp run` command | Command |
| supersetMcp.containerSecurityContext | object | `{}` | |
| supersetMcp.deploymentAdditionalPodSpec | object | `{}` | Custom pod spec to be added to supersetMcp deployment |
| supersetMcp.deploymentAnnotations | object | `{}` | Annotations to be added to supersetMcp deployment |
| supersetMcp.deploymentLabels | object | `{}` | Labels to be added to supersetMcp deployment |
| supersetMcp.enabled | bool | `false` | Enables the Superset MCP Server. To expose it via the shared ingress at /mcp, also set supersetMcp.ingress.enabled=true. WARNING: this requires fastMCP to be installed, which can be done by installing `apache-superset[fastmcp]` |
| supersetMcp.extraContainers | list | `[]` | Launch additional containers into supersetMcp pods |
| supersetMcp.extraInitContainers | list | `[]` | Extra init containers appended after supersetMcp initContainers |
| supersetMcp.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
| supersetMcp.ingress.enabled | bool | `false` | If true, the MCP server will be exposed via the ingress /mcp subpath |
| supersetMcp.ingress.path | string | `"/mcp"` | |
| supersetMcp.ingress.pathType | string | `"Prefix"` | |
| supersetMcp.initContainers | list | a container waiting for postgres and redis | List of init containers |
| supersetMcp.lifecycle | object | `{}` | Container lifecycle hooks for the worker pod |
| supersetMcp.livenessProbe.failureThreshold | int | `3` | |
| supersetMcp.livenessProbe.httpGet.path | string | `"/health"` | |
| supersetMcp.livenessProbe.httpGet.port | string | `"mcp"` | |
| supersetMcp.livenessProbe.initialDelaySeconds | int | `15` | |
| supersetMcp.livenessProbe.periodSeconds | int | `15` | |
| supersetMcp.livenessProbe.successThreshold | int | `1` | |
| supersetMcp.livenessProbe.timeoutSeconds | int | `3` | |
| supersetMcp.podAnnotations | object | `{}` | Annotations to be added to supersetMcp pods |
| supersetMcp.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":1,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetMcp pods |
| supersetMcp.podDisruptionBudget.enabled | bool | `false` | Whether the pod disruption budget should be created |
| supersetMcp.podDisruptionBudget.maxUnavailable | int | `1` | If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/\#specifying-a-poddisruptionbudget |
| supersetMcp.podDisruptionBudget.minAvailable | int | `1` | If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/\#specifying-a-poddisruptionbudget |
| supersetMcp.podLabels | object | `{}` | Labels to be added to supersetMcp pods |
| supersetMcp.podSecurityContext | object | `{}` | |
| supersetMcp.priorityClassName | string | `nil` | Set priorityClassName for supersetMcp pods |
| supersetMcp.readinessProbe.failureThreshold | int | `3` | |
| supersetMcp.readinessProbe.httpGet.path | string | `"/health"` | |
| supersetMcp.readinessProbe.httpGet.port | string | `"mcp"` | |
| supersetMcp.readinessProbe.initialDelaySeconds | int | `15` | |
| supersetMcp.readinessProbe.periodSeconds | int | `15` | |
| supersetMcp.readinessProbe.successThreshold | int | `1` | |
| supersetMcp.readinessProbe.timeoutSeconds | int | `3` | |
| supersetMcp.replicaCount | int | `1` | |
| supersetMcp.resources | object | `{}` | Resource settings for the supersetMcp pods - these settings overwrite might existing values from the global resources object defined above. |
| supersetMcp.service.annotations | object | `{}` | |
| supersetMcp.service.loadBalancerIP | string | `nil` | |
| supersetMcp.service.nodePort.http | int | `"nil"` | |
| supersetMcp.service.port | int | `5008` | |
| supersetMcp.service.type | string | `"ClusterIP"` | |
| supersetMcp.startupProbe.failureThreshold | int | `60` | |
| supersetMcp.startupProbe.httpGet.path | string | `"/health"` | |
| supersetMcp.startupProbe.httpGet.port | string | `"mcp"` | |
| supersetMcp.startupProbe.initialDelaySeconds | int | `15` | |
| supersetMcp.startupProbe.periodSeconds | int | `5` | |
| supersetMcp.startupProbe.successThreshold | int | `1` | |
| supersetMcp.startupProbe.timeoutSeconds | int | `3` | |
| supersetMcp.strategy | object | `{}` | |
| supersetMcp.terminationGracePeriodSeconds | string | `nil` | Pod termination grace period (seconds) for the worker pod so in-flight tasks can drain before SIGKILL |
| supersetMcp.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetMcp deployments |
| supersetNode.affinity | object | `{}` | Affinity to be added to supersetNode deployment |
| supersetNode.autoscaling.enabled | bool | `false` | |
| supersetNode.autoscaling.maxReplicas | int | `100` | |
@@ -300,16 +364,19 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetWebsockets.affinity | object | `{}` | Affinity to be added to supersetWebsockets deployment |
| supersetWebsockets.command | list | `[]` | |
| supersetWebsockets.config | object | see `values.yaml` | The config.json to pass to the server, see https://github.com/apache/superset/tree/master/superset-websocket Note that the configuration can also read from environment variables (which will have priority), see https://github.com/apache/superset/blob/master/superset-websocket/src/config.ts for a list of supported variables |
| supersetWebsockets.configAnnotations | object | `{}` | Annotations to be added to the secret which contains config.json |
| supersetWebsockets.containerSecurityContext | object | `{}` | |
| supersetWebsockets.deploymentAdditionalPodSpec | object | `{}` | Custom pod spec to be added to supersetWebsockets deployment |
| supersetWebsockets.deploymentAnnotations | object | `{}` | |
| supersetWebsockets.enabled | bool | `false` | This is only required if you intend to use `GLOBAL_ASYNC_QUERIES` in `ws` mode see https://superset.apache.org/docs/contributing/misc#async-chart-queries |
| supersetWebsockets.extraContainers | list | `[]` | Launch additional containers into supersetWebsockets pods |
| supersetWebsockets.extraInitContainers | list | `[]` | Extra init containers appended after supersetWebsockets initContainers |
| supersetWebsockets.image.pullPolicy | string | `"IfNotPresent"` | |
| supersetWebsockets.image.repository | string | `"oneacrefund/superset-websocket"` | There is no official image (yet), this one is community-supported |
| supersetWebsockets.image.tag | string | `"latest"` | |
| supersetWebsockets.ingress.path | string | `"/ws"` | |
| supersetWebsockets.ingress.pathType | string | `"Prefix"` | |
| supersetWebsockets.initContainers | list | a container waiting for redis | List of initContainers |
| supersetWebsockets.livenessProbe.failureThreshold | int | `3` | |
| supersetWebsockets.livenessProbe.httpGet.path | string | `"/health"` | |
| supersetWebsockets.livenessProbe.httpGet.port | string | `"ws"` | |

View File

@@ -761,3 +761,9 @@ app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: worker
{{- end }}
{{- define "supersetMcp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: mcp
{{- end }}

View File

@@ -0,0 +1,197 @@
{{/*
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.
*/}}
{{- if .Values.supersetMcp.enabled -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ template "superset.fullname" . }}-mcp
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "mcp" "root" .) | nindent 4 }}
{{- if .Values.supersetMcp.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetMcp.deploymentAnnotations | nindent 4 }}
{{- end }}
spec:
replicas: {{ .Values.supersetMcp.replicaCount }}
selector:
matchLabels:
{{- include "supersetMcp.selectorLabels" . | nindent 6 }}
{{- if .Values.supersetMcp.strategy }}
strategy: {{- toYaml .Values.supersetMcp.strategy | nindent 4 }}
{{- end }}
template:
metadata:
annotations:
checksum/superset_config.py: {{ include "superset.config" . | sha256sum }}
checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }}
checksum/database: {{ .Values.database | toYaml | sha256sum }}
checksum/redis: {{ .Values.cache | toYaml | sha256sum }}
checksum/config: {{ .Values.config | toYaml | sha256sum }}
checksum/featureFlags: {{ .Values.featureFlags | toYaml | sha256sum }}
checksum/extraConfigs: {{ .Values.extraConfigs | toYaml | sha256sum }}
checksum/extraSecrets: {{ .Values.extraSecrets | toYaml | sha256sum }}
checksum/extraSecretEnv: {{ .Values.extraSecretEnv | toYaml | sha256sum }}
checksum/configOverrides: {{ .Values.configOverrides | toYaml | sha256sum }}
checksum/configOverridesFiles: {{ .Values.configOverridesFiles | toYaml | sha256sum }}
{{- if .Values.supersetMcp.forceReload }}
# Optionally force the thing to reload
force-reload: {{ randAlphaNum 5 | quote }}
{{- end }}
{{- if .Values.supersetMcp.podAnnotations }}
{{- toYaml .Values.supersetMcp.podAnnotations | nindent 8 }}
{{- end }}
{{- with .Values.globalPodAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "supersetMcp.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetMcp.podLabels }}
{{- toYaml .Values.supersetMcp.podLabels | nindent 8 }}
{{- end }}
spec:
{{- if .Values.supersetMcp.deploymentAdditionalPodSpec }}
{{- tpl (toYaml .Values.supersetMcp.deploymentAdditionalPodSpec) . | nindent 6 }}
{{- end }}
{{- if or (.Values.serviceAccount.create) (.Values.serviceAccountName) }}
serviceAccountName: {{ template "superset.serviceAccountName" . }}
{{- end }}
securityContext:
runAsUser: {{ .Values.runAsUser }}
{{- if .Values.supersetMcp.podSecurityContext }}
{{- toYaml .Values.supersetMcp.podSecurityContext | nindent 8 }}
{{- end }}
{{- if or .Values.supersetMcp.initContainers .Values.supersetMcp.extraInitContainers }}
initContainers:
{{- if .Values.supersetMcp.initContainers }}
{{- tpl (toYaml .Values.supersetMcp.initContainers) . | nindent 6 }}
{{- end }}
{{- if .Values.supersetMcp.extraInitContainers }}
{{- tpl (toYaml .Values.supersetMcp.extraInitContainers) . | nindent 6 }}
{{- end }}
{{- end }}
{{- with .Values.hostAliases }}
hostAliases: {{- toYaml . | nindent 6 }}
{{- end }}
containers:
- name: "{{ .Chart.Name }}-mcp"
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.supersetMcp.containerSecurityContext }}
securityContext: {{- toYaml .Values.supersetMcp.containerSecurityContext | nindent 12 }}
{{- end }}
command: {{ tpl (toJson .Values.supersetMcp.command) . }}
env:
{{- range $key, $value := .Values.extraEnv }}
- name: {{ $key | quote}}
value: {{ $value | quote }}
{{- end }}
{{- if .Values.extraEnvRaw }}
{{- toYaml .Values.extraEnvRaw | nindent 12 }}
{{- end }}
envFrom:
- secretRef:
name: {{ tpl .Values.envFromSecret . | quote }}
{{- range .Values.envFromSecrets }}
- secretRef:
name: {{ tpl . $ | quote }}
{{- end }}
ports:
- name: mcp
containerPort: {{ .Values.supersetMcp.service.port }}
protocol: TCP
volumeMounts:
- name: superset-config
mountPath: {{ .Values.configMountPath | quote }}
readOnly: true
{{- if .Values.extraConfigs }}
- name: superset-extra-config
mountPath: {{ .Values.extraConfigMountPath | quote }}
readOnly: true
{{- end }}
{{- with .Values.extraVolumeMounts }}
{{- tpl (toYaml .) $ | nindent 12 -}}
{{- end }}
{{- if .Values.supersetMcp.startupProbe }}
startupProbe: {{- .Values.supersetMcp.startupProbe | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.supersetMcp.readinessProbe }}
readinessProbe: {{- .Values.supersetMcp.readinessProbe | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.supersetMcp.livenessProbe }}
livenessProbe: {{- .Values.supersetMcp.livenessProbe | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.supersetMcp.lifecycle }}
lifecycle: {{- .Values.supersetMcp.lifecycle | toYaml | nindent 12 }}
{{- end }}
resources:
{{- if .Values.supersetMcp.resources }}
{{- toYaml .Values.supersetMcp.resources | nindent 12 }}
{{- else }}
{{- toYaml .Values.resources | nindent 12 }}
{{- end }}
{{- if .Values.supersetMcp.extraContainers }}
{{- tpl (toYaml .Values.supersetMcp.extraContainers) . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
{{- end }}
{{- if or .Values.affinity .Values.supersetMcp.affinity }}
affinity:
{{- with .Values.affinity }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.supersetMcp.affinity }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- if .Values.supersetMcp.priorityClassName }}
priorityClassName: {{ .Values.supersetMcp.priorityClassName }}
{{- end }}
{{- if or .Values.topologySpreadConstraints .Values.supersetMcp.topologySpreadConstraints }}
topologySpreadConstraints:
{{- with .Values.topologySpreadConstraints }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.supersetMcp.topologySpreadConstraints }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- with .Values.tolerations }}
tolerations: {{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.supersetMcp.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.supersetMcp.terminationGracePeriodSeconds }}
{{- end }}
{{- if .Values.imagePullSecrets }}
imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
{{- end }}
volumes:
- name: superset-config
secret:
secretName: {{ tpl .Values.configFromSecret . }}
{{- if .Values.extraConfigs }}
- name: superset-extra-config
configMap:
name: {{ template "superset.fullname" . }}-extra-config
{{- end }}
{{- with .Values.extraVolumes }}
{{- tpl (toYaml .) $ | nindent 8 -}}
{{- end }}
{{- end -}}

View File

@@ -64,6 +64,15 @@ spec:
{{- if .Values.supersetWebsockets.podSecurityContext }}
{{- toYaml .Values.supersetWebsockets.podSecurityContext | nindent 8 }}
{{- end }}
{{- if or .Values.supersetWebsockets.initContainers .Values.supersetWebsockets.extraInitContainers }}
initContainers:
{{- if .Values.supersetWebsockets.initContainers }}
{{- tpl (toYaml .Values.supersetWebsockets.initContainers) . | nindent 6 }}
{{- end }}
{{- if .Values.supersetWebsockets.extraInitContainers }}
{{- tpl (toYaml .Values.supersetWebsockets.extraInitContainers) . | nindent 6 }}
{{- end }}
{{- end }}
{{- with .Values.hostAliases }}
hostAliases: {{- toYaml . | nindent 6 }}
{{- end }}

View File

@@ -0,0 +1,83 @@
{{/*
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.
*/}}
{{- if .Values.httproute.enabled -}}
{{- $fullName := include "superset.fullname" . -}}
apiVersion: {{ .Values.httproute.apiVersion | default "gateway.networking.k8s.io/v1" }}
kind: HTTPRoute
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 }}
{{- with .Values.httproute.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.httproute.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.httproute.parentRefs }}
parentRefs:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.httproute.hostnames }}
hostnames:
{{- tpl (toYaml .) $ | nindent 4 }}
{{- end }}
rules:
{{- range .Values.httproute.rules }}
- backendRefs:
- group: ''
kind: Service
name: {{ $fullName }}
port: {{ $.Values.service.port }}
weight: {{ .weight | default 1 }}
{{- with .filters }}
filters:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .matches }}
matches:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .timeouts }}
timeouts:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- if .Values.supersetWebsockets.enabled }}
- backendRefs:
- group: ''
kind: Service
name: {{ $fullName }}-ws
port: {{ .Values.supersetWebsockets.service.port }}
weight: 1
matches:
- path:
type: PathPrefix
value: {{ .Values.supersetWebsockets.ingress.path }}
{{- end }}
{{- end }}

View File

@@ -64,6 +64,15 @@ spec:
port:
name: ws
{{- end }}
{{- if and $.Values.supersetMcp.enabled $.Values.supersetMcp.ingress.enabled }}
- path: {{ $.Values.supersetMcp.ingress.path }}
pathType: {{ $.Values.supersetMcp.ingress.pathType }}
backend:
service:
name: "{{ template "superset.fullname" $ }}-mcp"
port:
name: mcp
{{- end }}
{{- end }}
{{- if .Values.ingress.extraHostsRaw }}
{{- toYaml .Values.ingress.extraHostsRaw | nindent 4 }}

View File

@@ -0,0 +1,42 @@
{{/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/}}
{{- with .Values.supersetMcp.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-mcp-pdb
labels:
{{- include "superset.componentLabels" (dict "component" "mcp" "root" $) | nindent 4 }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "supersetMcp.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}

View File

@@ -24,6 +24,9 @@ metadata:
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.labels" . | nindent 4 }}
{{- if .Values.secretEnv.annotations }}
annotations: {{- toYaml .Values.secretEnv.annotations | nindent 4 }}
{{- end }}
type: Opaque
stringData:
DB_HOST: {{ include "superset.db.host" . | quote }}

View File

@@ -24,6 +24,9 @@ metadata:
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.labels" . | nindent 4 }}
{{- if .Values.secretConfig.annotations }}
annotations: {{- toYaml .Values.secretConfig.annotations | nindent 4 }}
{{- end }}
type: Opaque
stringData:
superset_config.py: |

View File

@@ -25,6 +25,9 @@ metadata:
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.labels" . | nindent 4 }}
{{- if .Values.supersetWebsockets.configAnnotations }}
annotations: {{- toYaml .Values.supersetWebsockets.configAnnotations | nindent 4 }}
{{- end }}
type: Opaque
stringData:
config.json: |

View File

@@ -0,0 +1,46 @@
{{/*
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.
*/}}
{{- if .Values.supersetMcp.enabled }}
apiVersion: v1
kind: Service
metadata:
name: "{{ template "superset.fullname" . }}-mcp"
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "mcp" "root" .) | nindent 4 }}
{{- with .Values.supersetMcp.service.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.supersetMcp.service.type }}
ports:
- port: {{ .Values.supersetMcp.service.port }}
targetPort: mcp
protocol: TCP
name: mcp
{{- if and (or (eq .Values.supersetMcp.service.type "NodePort") (eq .Values.supersetMcp.service.type "LoadBalancer")) (not (empty .Values.supersetMcp.service.nodePort.http)) }}
nodePort: {{ .Values.supersetMcp.service.nodePort.http }}
{{- end }}
selector:
{{- include "supersetMcp.selectorLabels" . | nindent 4 }}
{{- if .Values.supersetMcp.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.supersetMcp.service.loadBalancerIP }}
{{- end }}
{{- end }}

View File

@@ -21,6 +21,7 @@ templates:
- deployment-beat.yaml
- deployment-flower.yaml
- deployment-ws.yaml
- deployment-mcp.yaml
tests:
- it: main deployment has new config checksums, not the old connections checksum
template: deployment.yaml
@@ -130,3 +131,24 @@ tests:
- equal:
path: 'spec.template.metadata.annotations.team'
value: data
- it: mcp deployment has config and database checksums
template: deployment-mcp.yaml
set:
supersetMcp:
enabled: true
asserts:
- exists:
path: 'spec.template.metadata.annotations["checksum/database"]'
- exists:
path: 'spec.template.metadata.annotations["checksum/config"]'
- it: mcp deployment applies globalPodAnnotations
template: deployment-mcp.yaml
set:
supersetMcp:
enabled: true
globalPodAnnotations:
team: data
asserts:
- equal:
path: 'spec.template.metadata.annotations.team'
value: data

View File

@@ -20,6 +20,8 @@ templates:
- deployment-worker.yaml
- deployment-beat.yaml
- deployment-flower.yaml
- deployment-ws.yaml
- deployment-mcp.yaml
- init-job.yaml
tests:
- it: renders only the default wait-for container by default
@@ -150,6 +152,42 @@ tests:
path: spec.template.spec.initContainers[1].name
value: prepare-assets
- it: appends websockets extraInitContainers after wait-for-redis
template: deployment-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.extraInitContainers:
- name: prepare-assets
image: busybox
asserts:
- lengthEqual:
path: spec.template.spec.initContainers
count: 2
- equal:
path: spec.template.spec.initContainers[0].name
value: wait-for-redis
- equal:
path: spec.template.spec.initContainers[1].name
value: prepare-assets
- it: appends mcp extraInitContainers after wait-for-postgres-redis
template: deployment-mcp.yaml
set:
supersetMcp.enabled: true
supersetMcp.extraInitContainers:
- name: prepare-assets
image: busybox
asserts:
- lengthEqual:
path: spec.template.spec.initContainers
count: 2
- equal:
path: spec.template.spec.initContainers[0].name
value: wait-for-postgres-redis
- equal:
path: spec.template.spec.initContainers[1].name
value: prepare-assets
- it: appends init job extraInitContainers after wait-for-postgres
template: init-job.yaml
set:

View File

@@ -21,9 +21,11 @@ templates:
- deployment-beat.yaml
- deployment-flower.yaml
- deployment-ws.yaml
- deployment-mcp.yaml
- service.yaml
- service-ws.yaml
- service-flower.yaml
- service-mcp.yaml
- init-job.yaml
- ingress.yaml
- configmap-superset.yaml
@@ -34,6 +36,7 @@ templates:
- pdb-beat.yaml
- pdb-flower.yaml
- pdb-ws.yaml
- pdb-mcp.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/
@@ -185,6 +188,36 @@ tests:
path: metadata.labels["app.kubernetes.io/component"]
value: websocket
# =============================================================================
# MCP Deployment Labels
# =============================================================================
- it: should have all recommended labels on mcp deployment
template: deployment-mcp.yaml
set:
supersetMcp.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/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 mcp deployment
template: deployment-mcp.yaml
set:
supersetMcp.enabled: true
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: mcp
# =============================================================================
# Service Labels
# =============================================================================
@@ -249,6 +282,27 @@ tests:
path: metadata.labels["app.kubernetes.io/component"]
value: flower
- it: should have all recommended labels on mcp service
template: service-mcp.yaml
set:
supersetMcp.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 mcp service
template: service-mcp.yaml
set:
supersetMcp.enabled: true
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: mcp
# =============================================================================
# Init Job Labels
# =============================================================================
@@ -366,6 +420,51 @@ tests:
path: spec.template.metadata.labels["app.kubernetes.io/component"]
value: worker
- it: should set selector matchLabels to concrete values on mcp deployment
template: deployment-mcp.yaml
set:
supersetMcp.enabled: true
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: mcp
- it: should match pod template labels to the selector on mcp deployment
template: deployment-mcp.yaml
set:
supersetMcp.enabled: true
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: mcp
- it: should set selector to concrete values on mcp service
template: service-mcp.yaml
set:
supersetMcp.enabled: true
asserts:
- equal:
path: spec.selector["app.kubernetes.io/name"]
value: superset
- equal:
path: spec.selector["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.selector["app.kubernetes.io/component"]
value: mcp
# =============================================================================
# Extra Labels Support
# =============================================================================
@@ -505,6 +604,26 @@ tests:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: websocket
- it: should have recommended labels and matching selector on mcp pdb
template: pdb-mcp.yaml
set:
supersetMcp.enabled: true
supersetMcp.podDisruptionBudget.enabled: true
supersetMcp.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: mcp
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: mcp
- it: should use recommended labels on init job pod template
template: init-job.yaml
set:

View File

@@ -61,6 +61,8 @@ secretEnv:
# -- Change to false in order to support externally created secret (Binami "Sealed Secrets" for Kubernetes or External Secrets Operator)
# note: when externally creating the secret, the chart still expects to pull values from a secret with the name of the release defaults to `release-name-superset-env` - full logic located in _helpers.tpl file: `define "superset.fullname"`
create: true
# -- Annotations to be added to the secret
annotations: {}
# -- Specify service account name to be used
serviceAccountName: ~
@@ -82,6 +84,11 @@ bootstrapScript: |
# Note: this secret must have the key superset_config.py in it and can include other files as well
configFromSecret: '{{ template "superset.fullname" . }}-config'
# -- Specify whether Helm should apply additional settings to a secret containing configuration files
secretConfig:
# -- Annotations to be added to the secret which contains configuration files
annotations: {}
# -- The name of the secret which we will use to populate env vars in deployed pods
# This can be useful for secret keys, etc.
envFromSecret: '{{ template "superset.fullname" . }}-env'
@@ -246,6 +253,40 @@ ingress:
# hosts:
# - chart-example.local
# -- Gateway API HTTPRoute for exposing Superset via a Gateway.
# Requires the Gateway API CRDs (gateway.networking.k8s.io/v1) installed in the cluster.
# @default -- see `values.yaml`
httproute:
enabled: false
# -- HTTPRoute apiVersion. Override to gateway.networking.k8s.io/v1beta1 for
# older Gateway API installations that have not promoted HTTPRoute to v1.
apiVersion: gateway.networking.k8s.io/v1
# -- Additional labels to add to the HTTPRoute
labels: {}
# -- Annotations to add to the HTTPRoute
annotations: {}
# -- Gateways this HTTPRoute attaches to
parentRefs: []
# - name: my-gateway
# namespace: gateway-system
# sectionName: https
# -- Hostnames that match against the HTTP Host header (templated)
hostnames: []
# - chart-example.local
# -- Routing rules. Each rule is backed by the Superset service. Set `weight`
# per rule to leave room for traffic splitting (defaults to 1). When
# `supersetWebsockets.enabled` is true, an extra rule routing
# `supersetWebsockets.ingress.path` to the `-ws` service is appended
# automatically, mirroring the ingress behavior.
rules:
- matches:
- path:
type: PathPrefix
value: /
# weight: 1
# filters: []
# timeouts: {}
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
@@ -858,6 +899,8 @@ supersetWebsockets:
"jwtSecret": "CHANGE-ME",
"jwtCookieName": "async-token",
}
# -- Annotations to be added to the secret which contains config.json
configAnnotations: {}
service:
type: ClusterIP
annotations: {}
@@ -867,6 +910,38 @@ supersetWebsockets:
# -- (int)
http: nil
command: []
# -- List of initContainers
# @default -- a container waiting for redis
initContainers:
- name: wait-for-redis
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
envFrom:
- secretRef:
name: "{{ tpl .Values.envFromSecret . }}"
command:
- /bin/bash
- -c
- |
# See supersetNode.initContainers for the rationale.
SECONDS=0
until (exec 3<>/dev/tcp/"$REDIS_HOST"/"$REDIS_PORT") 2>/dev/null; do
if [ "$SECONDS" -ge 120 ]; then
echo "timeout waiting for redis at $REDIS_HOST:$REDIS_PORT after 120s" >&2
exit 1
fi
echo "waiting for redis at $REDIS_HOST:$REDIS_PORT (elapsed ${SECONDS}s)"
sleep 2
done
echo "redis at $REDIS_HOST:$REDIS_PORT is up"
resources:
limits:
memory: "256Mi"
requests:
cpu: "250m"
memory: "128Mi"
# -- Extra init containers appended after supersetWebsockets initContainers
extraInitContainers: []
resources: {}
# -- Launch additional containers into supersetWebsockets pods
extraContainers: []
@@ -912,6 +987,144 @@ supersetWebsockets:
# -- Set priorityClassName for supersetWebsockets pods
priorityClassName: ~
supersetMcp:
# -- Enables the Superset MCP Server. To expose it via the shared ingress at /mcp,
# also set supersetMcp.ingress.enabled=true.
# WARNING: this requires fastMCP to be installed, which can be done by installing `apache-superset[fastmcp]`
enabled: false
replicaCount: 1
# -- Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetMcp pods
podDisruptionBudget:
# -- Whether the pod disruption budget should be created
enabled: false
# -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/\#specifying-a-poddisruptionbudget
minAvailable: 1
# -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/\#specifying-a-poddisruptionbudget
maxUnavailable: 1
# -- Command
# @default -- a `superset mcp run` command
command:
- "/bin/sh"
- "-c"
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; superset mcp run --host 0.0.0.0 --port {{ .Values.supersetMcp.service.port }}"
# -- If true, forces deployment to reload on each upgrade
forceReload: false
ingress:
# -- If true, the MCP server will be exposed via the ingress /mcp subpath
enabled: false
path: /mcp
pathType: Prefix
service:
type: ClusterIP
annotations: {}
loadBalancerIP: ~
port: 5008
nodePort:
# -- (int)
http: nil
startupProbe:
httpGet:
path: /health
port: mcp
initialDelaySeconds: 15
timeoutSeconds: 3
failureThreshold: 60
periodSeconds: 5
successThreshold: 1
livenessProbe:
httpGet:
path: /health
port: mcp
initialDelaySeconds: 15
timeoutSeconds: 3
failureThreshold: 3
periodSeconds: 15
successThreshold: 1
readinessProbe:
httpGet:
path: /health
port: mcp
initialDelaySeconds: 15
timeoutSeconds: 3
failureThreshold: 3
periodSeconds: 15
successThreshold: 1
# -- List of init containers
# @default -- a container waiting for postgres and redis
initContainers:
- name: wait-for-postgres-redis
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
envFrom:
- secretRef:
name: "{{ tpl .Values.envFromSecret . }}"
command:
- /bin/bash
- -c
- |
# See supersetNode.initContainers for the rationale.
SECONDS=0
wait_for() {
local host=$1 port=$2 name=$3
until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
if [ "$SECONDS" -ge 120 ]; then
echo "timeout waiting for $name at $host:$port after 120s" >&2
exit 1
fi
echo "waiting for $name at $host:$port (elapsed ${SECONDS}s)"
sleep 2
done
echo "$name at $host:$port is up"
}
wait_for "$DB_HOST" "$DB_PORT" postgres
wait_for "$REDIS_HOST" "$REDIS_PORT" redis
resources:
limits:
memory: "256Mi"
requests:
cpu: "250m"
memory: "128Mi"
# -- Extra init containers appended after supersetMcp initContainers
extraInitContainers: []
# -- Launch additional containers into supersetMcp pods
extraContainers: []
# -- Annotations to be added to supersetMcp deployment
deploymentAnnotations: {}
# -- Labels to be added to supersetMcp deployment
deploymentLabels: {}
# -- Custom pod spec to be added to supersetMcp deployment
deploymentAdditionalPodSpec: {}
# -- Affinity to be added to supersetMcp deployment
affinity: {}
# -- TopologySpreadConstrains to be added to supersetMcp deployments
topologySpreadConstraints: []
# -- Annotations to be added to supersetMcp pods
podAnnotations: {}
# -- Labels to be added to supersetMcp pods
podLabels: {}
strategy: {}
# type: RollingUpdate
# rollingUpdate:
# maxSurge: 25%
# maxUnavailable: 25%
# -- Container lifecycle hooks for the worker pod
lifecycle: {}
# -- Pod termination grace period (seconds) for the worker pod so in-flight tasks can drain before SIGKILL
terminationGracePeriodSeconds: ~
# -- Resource settings for the supersetMcp pods - these settings overwrite might existing values from the global resources object defined above.
resources: {}
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
podSecurityContext: {}
containerSecurityContext: {}
# -- Set priorityClassName for supersetMcp pods
priorityClassName: ~
init:
# Configure resources
# Warning: fab command consumes a lot of ram and can

View File

@@ -24,13 +24,12 @@ name = "apache_superset"
description = "A modern, enterprise-ready business intelligence web application"
readme = "README.md"
dynamic = ["version", "scripts", "entry-points"]
requires-python = ">=3.10"
requires-python = ">=3.11"
license = { file="LICENSE.txt" }
authors = [
{ name = "Apache Software Foundation", email = "dev@superset.apache.org" },
]
classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
@@ -63,7 +62,7 @@ dependencies = [
"flask-session>=0.4.0, <1.0",
"flask-wtf>=1.3.0, <2.0",
"geopy",
"greenlet<=3.5.1, >=3.5.1",
"greenlet<=3.5.3, >=3.5.3",
"gunicorn>=26.0.0, <27; sys_platform != 'win32'",
"hashids>=1.3.1, <2",
# holidays>=0.45 required for security fix
@@ -84,7 +83,7 @@ dependencies = [
"packaging",
# --------------------------
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
"pandas[excel]>=2.1.4, <2.4",
"pandas[excel]>=2.3.3, <2.4",
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
# --------------------------
"parsedatetime",
@@ -113,7 +112,7 @@ dependencies = [
"sqlglot>=30.12.0, <31",
# newer pandas needs 0.9+
"tabulate>=0.10.0, <1.0",
"typing-extensions>=4, <5",
"typing-extensions>=4.16.0, <5",
"waitress; sys_platform == 'win32'",
"watchdog>=6.0.0",
"wtforms>=3.2.2, <4",
@@ -126,7 +125,7 @@ dependencies = [
athena = ["pyathena[pandas]>=2, <4"]
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.19.1",
"pandas-gbq>=0.35.0",
"sqlalchemy-bigquery>=1.17.0",
"google-cloud-bigquery>=3.42.1",
]
@@ -149,11 +148,15 @@ dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.10, <2"]
druid = ["pydruid>=0.6.5,<0.7"]
duckdb = ["duckdb>=1.5.4,<2", "duckdb-engine>=0.17.0"]
dynamodb = ["pydynamodb>=0.4.2"]
dynamodb = ["pydynamodb>=0.8.2"]
solr = ["sqlalchemy-solr >= 0.2.4.3"]
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
exasol = ["sqlalchemy-exasol>=2.4.0, <8.0"]
excel = ["xlrd>=2.0.2, <2.1"]
# Async dashboard "Export Data/Images to Excel": uploads the workbook to S3 and
# emails a pre-signed link. boto3 is imported lazily by superset.utils.s3, so
# installing this extra is only required to actually run exports.
excel-export = ["boto3"]
fastmcp = [
"fastmcp>=3.4.3,<4.0",
# tiktoken backs the response-size-guard token estimator. Without
@@ -167,7 +170,6 @@ gevent = ["gevent>=26.4.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
hana = ["hdbcli==2.29.25", "sqlalchemy_hana==3.0.3"]
hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7.0",
"tableschema",
"thrift>=0.23.0, <1.0.0",
@@ -201,7 +203,6 @@ singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.10.2, <2"]
sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
spark = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7",
"tableschema",
"thrift>=0.23.0, <1",
@@ -213,7 +214,7 @@ tdengine = [
teradata = ["teradatasql>=20.0.0.62"]
thumbnails = [] # deprecated, will be removed in 7.0
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
netezza = ["nzalchemy>=11.0.2, < 11.2"]
netezza = ["nzalchemy>= 11.1.2, < 11.2"]
starrocks = ["starrocks>=1.3.3, <2"]
doris = ["pydoris>=1.2.0, <2.0.0"]
oceanbase = ["oceanbase_py>=0.0.1.2"]
@@ -344,8 +345,8 @@ exclude = [
line-length = 88
indent-width = 4
# Assume Python 3.10
target-version = "py310"
# Assume Python 3.11
target-version = "py311"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.

View File

@@ -29,12 +29,12 @@ filterwarnings =
ignore
always::sqlalchemy.exc.RemovedIn20Warning
error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
# error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"SqlMetric" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"TableColumn" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SqlMetric" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"TableColumn" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:The autoload parameter is deprecated:sqlalchemy.exc.RemovedIn20Warning
error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
error:The connection.execute\(\) method:sqlalchemy.exc.RemovedIn20Warning

View File

@@ -164,7 +164,7 @@ google-auth==2.53.0
# via
# -r requirements/base.in
# shillelagh
greenlet==3.5.1
greenlet==3.5.3
# via
# apache-superset (pyproject.toml)
# shillelagh
@@ -275,7 +275,7 @@ packaging==25.0
# kombu
# limits
# shillelagh
pandas==2.1.4
pandas==2.3.3
# via apache-superset (pyproject.toml)
paramiko==3.5.1
# via
@@ -331,6 +331,8 @@ pyparsing==3.2.3
# via apache-superset (pyproject.toml)
pysocks==1.7.1
# via urllib3
python-calamine==0.8.2
# via pandas
python-dateutil==2.9.0.post0
# via
# apache-superset (pyproject.toml)
@@ -427,7 +429,7 @@ trio==0.33.0
# trio-websocket
trio-websocket==0.12.2
# via selenium
typing-extensions==4.15.0
typing-extensions==4.16.0
# via
# apache-superset (pyproject.toml)
# alembic

View File

@@ -340,7 +340,6 @@ gevent==26.4.0
google-api-core==2.23.0
# via
# google-cloud-bigquery
# google-cloud-bigquery-storage
# google-cloud-core
# pandas-gbq
# sqlalchemy-bigquery
@@ -350,7 +349,6 @@ google-auth==2.53.0
# google-api-core
# google-auth-oauthlib
# google-cloud-bigquery
# google-cloud-bigquery-storage
# google-cloud-core
# pandas-gbq
# pydata-google-auth
@@ -365,8 +363,6 @@ google-cloud-bigquery==3.42.1
# apache-superset
# pandas-gbq
# sqlalchemy-bigquery
google-cloud-bigquery-storage==2.26.0
# via pandas-gbq
google-cloud-core==2.4.1
# via google-cloud-bigquery
google-crc32c==1.6.0
@@ -377,7 +373,7 @@ googleapis-common-protos==1.66.0
# via
# google-api-core
# grpcio-status
greenlet==3.5.1
greenlet==3.5.3
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -638,10 +634,11 @@ packaging==25.0
# kombu
# limits
# matplotlib
# pandas-gbq
# pytest
# shillelagh
# sqlalchemy-bigquery
pandas==2.1.4
pandas==2.3.3
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -649,7 +646,7 @@ pandas==2.1.4
# db-dtypes
# pandas-gbq
# prophet
pandas-gbq==0.19.1
pandas-gbq==0.35.0
# via apache-superset
parameterized==0.9.0
# via apache-superset
@@ -705,18 +702,17 @@ prompt-toolkit==3.0.51
prophet==1.2.0
# via apache-superset
proto-plus==1.25.0
# via
# google-api-core
# google-cloud-bigquery-storage
# via google-api-core
protobuf==5.29.6
# via
# google-api-core
# google-cloud-bigquery-storage
# googleapis-common-protos
# grpcio-status
# proto-plus
psutil==6.1.0
# via apache-superset
# via
# apache-superset
# pandas-gbq
psycopg2-binary==2.9.12
# via apache-superset
py-key-value-aio==0.4.4
@@ -823,6 +819,10 @@ pytest-mock==3.10.0
# via
# apache-superset
# apache-superset-extensions-cli
python-calamine==0.8.2
# via
# -c requirements/base-constraint.txt
# pandas
python-dateutil==2.9.0.post0
# via
# -c requirements/base-constraint.txt
@@ -1043,7 +1043,7 @@ trio-websocket==0.12.2
# via
# -c requirements/base-constraint.txt
# selenium
typing-extensions==4.15.0
typing-extensions==4.16.0
# via
# -c requirements/base-constraint.txt
# alembic

View File

@@ -93,7 +93,7 @@ def find_models(module: ModuleType) -> list[type[Model]]: # noqa: C901
# where the current model is out-of-sync with the existing table after a
# downgrade
sqlalchemy_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]
engine = create_engine(sqlalchemy_uri)
engine = create_engine(sqlalchemy_uri, future=True)
Base = automap_base() # noqa: N806
Base.prepare(engine, reflect=True)
seen = set()

View File

@@ -133,8 +133,8 @@ def main(docker: bool, frontend: bool, backend: bool) -> None: # noqa: C901
requirements = [
Requirement(
"python",
(Version("3.10.0"), Version("3.10.999")),
(Version("3.9.0"), Version("3.11.999")),
(Version("3.11.0"), Version("3.11.999")),
(Version("3.11.0"), Version("3.12.999")),
"backend",
"python --version",
),

View File

@@ -21,7 +21,7 @@ under the License.
[![PyPI version](https://badge.fury.io/py/apache-superset-core.svg)](https://badge.fury.io/py/apache-superset-core)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
The official core package for building Apache Superset backend extensions and integrations. This package provides essential building blocks including base classes, API utilities, type definitions, and decorators for both the host application and extensions.

View File

@@ -26,7 +26,7 @@ authors = [
]
license = "Apache-2.0"
license-files = ["LICENSE.txt"]
requires-python = ">=3.10"
requires-python = ">=3.11"
keywords = ["superset", "apache", "analytics", "business-intelligence", "extensions", "visualization"]
classifiers = [
"Development Status :: 3 - Alpha",
@@ -34,7 +34,6 @@ classifiers = [
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Database",

View File

@@ -0,0 +1,119 @@
# 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.
"""
Extension Context API for superset-core extensions.
Provides access to the current extension's context, including metadata
and scoped resources like storage. Extensions call `get_context()` to
access their context during execution.
The context is set by the host (Superset) during extension loading and
is only available within extension code.
Usage:
from superset_core.extensions.context import get_context
def setup():
ctx = get_context()
# Access extension metadata
print(f"Running {ctx.extension.displayName} v{ctx.extension.version}")
# Access extension-scoped storage
ctx.storage.ephemeral.set("lastRun", time.time(), EphemeralSetOptions(ttl=3600))
data = ctx.storage.ephemeral.get("cachedData")
"""
from __future__ import annotations
from typing import Protocol, TYPE_CHECKING
if TYPE_CHECKING:
from superset_core.extensions.storage.ephemeral import EphemeralStateAccessor
from superset_core.extensions.storage.persistent import PersistentStateAccessor
from superset_core.extensions.types import BaseExtension
class ExtensionStorage(Protocol):
"""Extension-scoped storage accessor for all available tiers."""
@property
def ephemeral(self) -> "EphemeralStateAccessor":
"""Server-side cache (Redis/Memcached) with TTL."""
...
@property
def persistent(self) -> "PersistentStateAccessor":
"""Database-backed persistent storage."""
...
class ExtensionContext(Protocol):
"""
Context object providing extension-specific resources.
This context is only available during extension execution.
Calling `get_context()` outside of an extension will raise an error.
"""
@property
def extension(self) -> "BaseExtension":
"""Metadata about the current extension."""
...
@property
def storage(self) -> ExtensionStorage:
"""Extension-scoped storage across all available tiers."""
...
def get_context() -> ExtensionContext:
"""
Get the current extension's context.
This function returns the context for the currently executing extension,
providing access to extension metadata and scoped resources like storage.
Host implementations will replace this function during initialization
with a concrete implementation providing actual functionality.
:returns: The current extension's context.
:raises RuntimeError: If called outside of an extension context.
Example:
from superset_core.extensions.context import get_context
ctx = get_context()
# Access extension metadata
print(f"Extension: {ctx.extension.id}")
print(f"Version: {ctx.extension.version}")
# Access extension-scoped storage
ctx.storage.ephemeral.set("tempData", data, EphemeralSetOptions(ttl=3600))
value = ctx.storage.ephemeral.get("tempData")
# Access shared (cross-user) storage
ctx.storage.ephemeral.shared.set(
"globalCounter", count, EphemeralSetOptions(ttl=3600)
)
"""
raise NotImplementedError(
"get_context() must be called within an extension context. "
"This function is replaced by the host during extension loading."
)

View File

@@ -0,0 +1,79 @@
# 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.
"""
Storage API for superset-core extensions.
Provides storage tiers for extensions with different persistence characteristics.
Storage is accessed via `ctx.storage` from `get_context()`.
Tier 1 - Local State (Frontend Only):
- local: Browser localStorage - persists across sessions
- session: Browser sessionStorage - cleared on tab close
These are frontend-only and cannot be imported in backend code.
Tier 2 - Ephemeral State (Server Cache):
- ephemeral: Short-lived KV storage backed by server-side cache
- Supports TTL, not guaranteed to survive server restarts
- Use for temporary state like job progress or intermediate results
Tier 3 - Persistent State (Database):
- persistent: Durable KV storage backed by database table
- Survives server restarts, supports encryption and resource linking
- Use for user preferences, extension config, per-resource settings
All tiers follow the same API pattern:
- User-scoped by default (private to current user)
- `shared` accessor for data visible to all users
For backend code that needs to query, enumerate, or bulk-manage its own
Tier 3 rows rather than get/set a single key at a time (for example, a
cleanup job pruning rows linked to resources that no longer exist), see
`superset_core.extensions.storage.dao.ExtensionStorageDAO`.
Usage:
from superset_core.extensions.context import get_context
from superset_core.extensions.storage.ephemeral import EphemeralSetOptions
ctx = get_context()
# Tier 2: Ephemeral state
ctx.storage.ephemeral.get('preference')
ctx.storage.ephemeral.set('preference', 'compact', EphemeralSetOptions(ttl=3600))
# Tier 2: Shared ephemeral state
ctx.storage.ephemeral.shared.get('job_progress')
ctx.storage.ephemeral.shared.set(
'job_progress', {'pct': 42}, EphemeralSetOptions(ttl=3600)
)
# Tier 3: Persistent state
ctx.storage.persistent.get('config')
ctx.storage.persistent.set('config', {'version': 2})
# Tier 3: DAO-style access to this extension's own rows
from superset_core.extensions.storage.dao import ExtensionStorageDAO
entries = ExtensionStorageDAO.filter_by(resource_type='my-resource-type')
"""
from superset_core.extensions.storage import (
dao, # noqa: F401
ephemeral, # noqa: F401
models, # noqa: F401
persistent, # noqa: F401
)

View File

@@ -0,0 +1,70 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Persistent storage DAO API for superset-core extensions (Tier 3 Storage).
Provides direct DAO-style access to an extension's own Tier 3 persistent
storage rows, for backend code that needs to query, enumerate, or bulk-manage
entries rather than get/set a single key at a time (see
`superset_core.extensions.storage.persistent` for that simpler shape).
Every query and mutation made through this DAO is automatically scoped to
the extension currently executing — `extension_id` is never a parameter the
caller supplies, so an extension cannot read or modify another extension's
storage through this DAO. The scoping is resolved from the ambient
extension context (the same context `get_context()` reads from), so this DAO
can only be used from within extension backend code, not host code.
Usage:
from superset_core.extensions.storage.dao import ExtensionStorageDAO
# All entries linked to a specific resource this extension created
entries = ExtensionStorageDAO.filter_by(
resource_type="my-resource-type",
resource_uuid=resource_uuid,
)
# Bulk-delete rows for a resource that no longer exists
ExtensionStorageDAO.delete(entries)
"""
from superset_core.common.daos import BaseDAO
from superset_core.extensions.storage.models import ExtensionStorageEntry
class ExtensionStorageDAO(BaseDAO[ExtensionStorageEntry]):
"""
Abstract DAO for an extension's own Tier 3 persistent storage rows.
Host implementations will replace this class during initialization
with a concrete implementation providing actual functionality.
Inherits `find_all`, `find_one_or_none`, `filter_by`, `query`, `update`,
and `delete` from `BaseDAO`, every one restricted to the calling
extension's own rows. `create()` is overridden by the host
implementation to always raise: it is a raw insert with no upsert
dedup, quota check, or locking against the key
`superset_core.extensions.storage.persistent`'s `set()` writes to, so
use that (or the ambient `ctx.storage.persistent.set()` accessor) to
write a value instead.
"""
# Class variables that will be set by host implementation
model_cls = None
base_filter = None
id_column_name = "id"

View File

@@ -0,0 +1,162 @@
# 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.
"""
Ephemeral State API for superset-core extensions (Tier 2 Storage).
Provides short-lived KV storage backed by the configured server-side cache
backend (Redis, Memcached, or filesystem). Automatically expires based on TTL.
Not guaranteed to survive server restarts.
Host implementations will replace the EphemeralState class during initialization
with a concrete implementation providing actual functionality.
Cache keys are namespaced automatically:
- User-scoped (default): superset-ext:{extension_id}:user:{user_id}:{key}
- Shared (global): superset-ext:{extension_id}:shared:{key}
Usage (via extension context - preferred):
from superset_core.extensions.context import get_context
ctx = get_context()
# User-scoped state (default - private to current user)
ctx.storage.ephemeral.get('preference')
ctx.storage.ephemeral.set('preference', 'compact', EphemeralSetOptions(ttl=3600))
ctx.storage.ephemeral.remove('preference')
# Shared state (explicit opt-in - visible to all users)
ctx.storage.ephemeral.shared.get('job_progress')
ctx.storage.ephemeral.shared.set(
'job_progress', {'pct': 42}, EphemeralSetOptions(ttl=3600)
)
ctx.storage.ephemeral.shared.remove('job_progress')
"""
from dataclasses import dataclass
from typing import Any, ClassVar, Protocol
@dataclass(frozen=True)
class EphemeralSetOptions:
"""
Options for an ephemeral state `set` call.
`ttl` is required (no default): every write must have an explicit
expiration, matching the frontend `EphemeralSetOptions` and the REST API.
"""
ttl: int
#: Name of the codec used to encode `value`, e.g. "json" (default).
codec: str = "json"
class EphemeralStateAccessor(Protocol):
"""Protocol for scoped ephemeral state access."""
def get(self, key: str) -> Any:
"""Get a value from ephemeral state."""
...
def set(self, key: str, value: Any, options: EphemeralSetOptions) -> None:
"""Set a value in ephemeral state with TTL."""
...
def remove(self, key: str) -> None:
"""Remove a value from ephemeral state."""
...
class _UnconfiguredEphemeralStateAccessor:
"""Placeholder for `.shared` before host replacement.
Raises the same `NotImplementedError` as the other stub methods, rather
than a bare `AttributeError`, if accessed before the host wires up the
concrete implementation.
"""
def get(self, key: str) -> Any:
raise NotImplementedError("Class will be replaced during initialization")
def set(self, key: str, value: Any, options: EphemeralSetOptions) -> None:
raise NotImplementedError("Class will be replaced during initialization")
def remove(self, key: str) -> None:
raise NotImplementedError("Class will be replaced during initialization")
class EphemeralState:
"""
Tier 2 ephemeral state storage for extensions.
Backed by the configured server-side cache (Redis, Memcached, or filesystem).
Data expires based on TTL and is not guaranteed to survive server restarts.
Host implementations will replace this class during initialization
with a concrete implementation providing actual functionality.
All operations are user-scoped by default (private to the current user).
Use `.shared` to access state that is visible to all users.
"""
@staticmethod
def get(key: str) -> Any:
"""
Get a value from user-scoped ephemeral state.
Data is automatically scoped to the current authenticated user.
Other users cannot see or modify this data.
:param key: The key to retrieve.
:returns: The stored value, or None if not found or expired.
"""
raise NotImplementedError("Class will be replaced during initialization")
@staticmethod
def set(key: str, value: Any, options: EphemeralSetOptions) -> None:
"""
Set a value in user-scoped ephemeral state with TTL.
Data is automatically scoped to the current authenticated user.
Other users cannot see or modify this data.
:param key: The key to store.
:param value: The value to store, encoded with `options.codec`
(default "json"). The encoded value must not exceed
MAX_VALUE_SIZE from config.
:param options: `EphemeralSetOptions`, e.g. `ttl=3600`. Required —
`ttl` must not exceed MAX_TTL from config. `codec="pickle"`
stores a value that isn't JSON-serializable.
"""
raise NotImplementedError("Class will be replaced during initialization")
@staticmethod
def remove(key: str) -> None:
"""
Remove a value from user-scoped ephemeral state.
Data is automatically scoped to the current authenticated user.
:param key: The key to remove.
"""
raise NotImplementedError("Class will be replaced during initialization")
#: Shared (global) ephemeral state accessor.
#: Data stored via this accessor is visible to all users of the extension.
#: WARNING: Do not store user-specific or sensitive data here.
#: Host implementations will replace this during initialization.
shared: ClassVar[EphemeralStateAccessor] = _UnconfiguredEphemeralStateAccessor()

View File

@@ -0,0 +1,74 @@
# 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.
"""
Persistent storage model API for superset-core extensions (Tier 3 Storage).
Provides the model class backing `superset_core.extensions.storage.dao.
ExtensionStorageDAO`, for extension backend code that needs to enumerate or
bulk-manage its own persistent storage rows (for example, a periodic cleanup
job pruning rows linked to resources that no longer exist), rather than the
single-key get/set/remove shape of `persistent`.
Usage:
from superset_core.extensions.storage.models import ExtensionStorageEntry
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from uuid import UUID
from superset_core.common.models import CoreModel
if TYPE_CHECKING:
from datetime import datetime
class ExtensionStorageEntry(CoreModel):
"""
Abstract model interface for a single Tier 3 persistent storage row.
Host implementations will replace this class during initialization
with a concrete implementation providing actual functionality.
`value` is the raw stored payload (encrypted at rest when `is_encrypted`
is set), encoded with the codec named by `codec`; decoding/decryption is
handled by `superset_core.extensions.storage.dao.ExtensionStorageDAO`,
not by reading this field directly.
"""
__abstract__ = True
# Type hints for expected column attributes
id: int
uuid: UUID
extension_id: str
user_fk: int | None
resource_type: str | None
resource_uuid: str | None
key: str
value: bytes
value_size: int
codec: str
is_encrypted: bool
# Audit fields
created_on: "datetime | None"
changed_on: "datetime | None"
created_by_fk: int | None
changed_by_fk: int | None

View File

@@ -0,0 +1,243 @@
# 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.
"""
Persistent State API for superset-core extensions (Tier 3 Storage).
Provides durable KV storage backed by a dedicated database table.
Data survives server restarts, cache evictions, and browser clears.
Suitable for user preferences, saved state, and any data that must not be lost.
Host implementations will replace the PersistentState class during initialization
with a concrete implementation providing actual functionality.
Database keys are namespaced automatically:
- User-scoped (default): (extension_id, user_id, key)
- Shared (global): (extension_id, null, key)
Usage (via extension context - preferred):
from superset_core.extensions.context import get_context
ctx = get_context()
# User-scoped state (default - private to current user)
ctx.storage.persistent.get('preferences')
ctx.storage.persistent.set('preferences', {'theme': 'dark'})
ctx.storage.persistent.remove('preferences')
# Shared state (explicit opt-in - visible to all users)
ctx.storage.persistent.shared.get('global_config')
ctx.storage.persistent.shared.set('global_config', {'version': 2})
ctx.storage.persistent.shared.remove('global_config')
# Encrypted at rest
ctx.storage.persistent.set(
'api_token', 'sk-...', PersistentSetOptions(encrypt=True)
)
# Listing entries (page and page_size are required)
result = ctx.storage.persistent.list(PersistentListOptions(page=0, page_size=10))
for entry in result.entries:
print(entry.key, entry.value)
"""
from dataclasses import dataclass, field
from typing import Any, ClassVar, Protocol
@dataclass(frozen=True)
class PersistentSetOptions:
"""
Options for a persistent state `set` call.
NOTE: This is intentionally minimal for the initial implementation.
Additional options can be added here later without changing the `set`
signature on `PersistentStateAccessor`/`PersistentState`.
"""
encrypt: bool = False
#: Name of the codec used to encode `value`, e.g. "json" (default).
codec: str = "json"
@dataclass(frozen=True)
class PersistentListOptions:
"""Options for a persistent state `list` call.
`page` and `page_size` are required (no default): `list` returns one
page of a caller's entries, not the whole result set, and a default
would let that fact go unnoticed at the call site. Check the returned
`PersistentListResult.count` against `page_size` to know whether more
pages exist.
`resource_type`/`resource_uuid` are optional filters; omitting them
lists every entry in the caller's scope (global or user-scoped,
depending on whether `list` is called via `.shared` or directly).
"""
#: Zero-indexed page number.
page: int
#: Entries per page. There is no fixed ceiling on this value, but a
#: page whose combined value size exceeds MAX_LIST_PAYLOAD_SIZE from
#: config is rejected — reduce page_size and retry if that happens.
page_size: int
resource_type: str | None = None
resource_uuid: str | None = None
@dataclass(frozen=True)
class PersistentListEntry:
"""A single entry returned by a persistent state `list` call."""
key: str
value: Any
#: Name of the codec `value` was encoded with, e.g. "json" or "pickle".
codec: str
@dataclass(frozen=True)
class PersistentListResult:
"""Result of a persistent state `list` call."""
entries: list[PersistentListEntry] = field(default_factory=list)
#: Total number of entries matching the given scope/filters, across all
#: pages — not just the number returned in `entries`.
count: int = 0
class PersistentStateAccessor(Protocol):
"""Protocol for scoped persistent state access."""
def get(self, key: str) -> Any:
"""Get a value from persistent state."""
...
def set(
self, key: str, value: Any, options: PersistentSetOptions | None = None
) -> None:
"""Set a value in persistent state."""
...
def list(self, options: PersistentListOptions) -> PersistentListResult:
"""List entries in persistent state."""
...
def remove(self, key: str) -> None:
"""Remove a value from persistent state."""
...
class _UnconfiguredPersistentStateAccessor:
"""Placeholder for `.shared` before host replacement.
Raises the same `NotImplementedError` as the other stub methods, rather
than a bare `AttributeError`, if accessed before the host wires up the
concrete implementation.
"""
def get(self, key: str) -> Any:
raise NotImplementedError("Class will be replaced during initialization")
def set(
self, key: str, value: Any, options: PersistentSetOptions | None = None
) -> None:
raise NotImplementedError("Class will be replaced during initialization")
def list(self, options: PersistentListOptions) -> PersistentListResult:
raise NotImplementedError("Class will be replaced during initialization")
def remove(self, key: str) -> None:
raise NotImplementedError("Class will be replaced during initialization")
class PersistentState:
"""
Tier 3 persistent state storage for extensions.
Backed by a dedicated database table. Data survives server restarts,
cache evictions, and browser clears.
Host implementations will replace this class during initialization
with a concrete implementation providing actual functionality.
All operations are user-scoped by default (private to the current user).
Use `.shared` to access state that is visible to all users.
"""
@staticmethod
def get(key: str) -> Any:
"""
Get a value from user-scoped persistent state.
Data is automatically scoped to the current authenticated user.
Other users cannot see or modify this data.
:param key: The key to retrieve.
:returns: The stored value, or None if not found.
"""
raise NotImplementedError("Class will be replaced during initialization")
@staticmethod
def set(key: str, value: Any, options: PersistentSetOptions | None = None) -> None:
"""
Set a value in user-scoped persistent state.
Data is automatically scoped to the current authenticated user.
Other users cannot see or modify this data.
Data persists indefinitely until explicitly removed.
:param key: The key to store.
:param value: The value to store, encoded with `options.codec`
(default "json"). The encoded value must not exceed
MAX_VALUE_SIZE from config.
:param options: Optional `PersistentSetOptions`, e.g. `encrypt=True`
to store the value encrypted at rest, or `codec="pickle"` to
store a value that isn't JSON-serializable.
"""
raise NotImplementedError("Class will be replaced during initialization")
@staticmethod
def list(options: PersistentListOptions) -> PersistentListResult:
"""
List entries in user-scoped persistent state.
Data is automatically scoped to the current authenticated user.
Other users' entries are never returned.
:param options: `PersistentListOptions`, e.g.
`PersistentListOptions(page=0, page_size=10)`.
:returns: `PersistentListResult` with the page's entries and the
total count across all pages.
"""
raise NotImplementedError("Class will be replaced during initialization")
@staticmethod
def remove(key: str) -> None:
"""
Remove a value from user-scoped persistent state.
Data is automatically scoped to the current authenticated user.
:param key: The key to remove.
"""
raise NotImplementedError("Class will be replaced during initialization")
#: Shared (global) persistent state accessor.
#: Data stored via this accessor is visible to all users of the extension.
#: WARNING: Do not store user-specific or sensitive data here.
#: Host implementations will replace this during initialization.
shared: ClassVar[PersistentStateAccessor] = _UnconfiguredPersistentStateAccessor()

View File

@@ -25,7 +25,7 @@ Two distinct schemas:
from __future__ import annotations
from pydantic import BaseModel, Field # noqa: I001
from pydantic import BaseModel, computed_field, Field # noqa: I001
from superset_core.extensions.constants import (
DISPLAY_NAME_PATTERN,
@@ -78,6 +78,11 @@ class BaseExtension(BaseModel):
description="Permissions required by this extension",
)
@computed_field(description="Composite extension ID (publisher.name)") # type: ignore[prop-decorator]
@property
def id(self) -> str:
return f"{self.publisher}.{self.name}"
# =============================================================================
# ExtensionConfig (extension.json)
@@ -137,11 +142,6 @@ class Manifest(BaseExtension):
This file is generated by the build tool from extension.json.
"""
id: str = Field(
...,
description="Composite extension ID (publisher.name)",
min_length=1,
)
frontend: ManifestFrontend | None = Field(
default=None,
description="Frontend manifest",

View File

@@ -3042,9 +3042,9 @@
"dev": true
},
"node_modules/fast-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"dev": true,
"funding": [
{

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