Compare commits

...

137 Commits

Author SHA1 Message Date
Đỗ Trọng Hải
6e2769fb48 Merge branch 'master' into fix/mask-sensitive-data-in-debug-log-for-failed-import-validation 2026-07-12 20:05:09 +07:00
hainenber
2f438d47a9 fix(sec): redact sensitive data in debug log for failed import validation
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-12 20:00:41 +07:00
Evan Rusackas
49dc0acd82 fix(pre-commit): prettier hook drops first file and never batches (#41944)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-12 11:17:27 +07:00
dependabot[bot]
5e6b29d1a3 chore(deps-dev): bump sqlalchemy-hana from 0.4.0 to 3.0.3 (#41953)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 16:59:46 -07:00
Anatolii
189f258e0c fix(drill): coerce temporal drill filter values (#40180)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-07-11 15:24:51 -07:00
yousoph
f5deda7864 fix(dataset): copy catalog field when duplicating a BigQuery dataset (#41106)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-07-11 15:24:21 -07:00
Elizabeth Thompson
b237aefb1e fix(a11y): add aria-label to RightMenu documentation and bug-report icon links (#41903) 2026-07-11 15:03:24 -07:00
Elizabeth Thompson
0ecf34d80e fix(a11y): add aria-label to ViewportControl text inputs (#41931) 2026-07-11 15:02:44 -07:00
Dhimas Ardinata
a03cabffa7 perf(csv): avoid regex in CSV value escaping (#40195)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:52:50 -07:00
Krupa Vadher
e852147182 test: add edge-case tests for datetime_to_epoch function (#39404)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-07-11 11:51:46 -07:00
dependabot[bot]
df209cedbf chore(deps-dev): update sqlalchemy-firebird requirement from <2.2,>=0.7.0 to >=0.8.0,<2.2 (#41950)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:51:36 -07:00
Mike Bridge
b641008da6 fix(versioning): preserve complete multi-flush history (#41940)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-07-11 10:50:48 -07:00
Luis Carbonell
678fcba8b4 feat(helm): Superset 6 structured config schema with deprecation path (#41777) 2026-07-11 10:50:20 -07:00
Greg Neighbors
5c12f216d5 feat(mcp): add delete_chart and delete_dashboard tools (#41472)
Co-authored-by: Greg Neighbors <gregneighbors@Gregs-Air-2.lan>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 10:47:27 -07:00
Ujjwal Jain
7a752d1c22 fix(dashboard): restore arrow key navigation in modal inputs opened from dropdown (#37978) 2026-07-11 10:39:28 -07:00
dependabot[bot]
51ce2b11ad chore(deps-dev): update impyla requirement from <0.23,>0.16.2 to >=0.24.0,<0.25 (#41951)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 05:18:31 -07:00
Greg Neighbors
3818152191 fix(mcp): user/role tools demanded a permission FAB never registers (#41858)
Co-authored-by: Greg Neighbors <gregneighbors@Gregs-Air-2.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:36:54 -07:00
Mike Bridge
4dde4d2c70 fix(versioning): make baseline capture atomic (#41941)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-07-10 16:12:32 -07:00
Yinka Metrics
35712ff977 fix(sqllab): show truncated table list warning (#41617)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-10 16:10:20 -07:00
dependabot[bot]
bc80d138fd chore(deps): bump slack-sdk from 3.42.0 to 3.43.0 (#41893)
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-10 14:59:10 -07:00
Amin Ghadersohi
bd61e09b4e fix(mcp): bind Big Number chart to a temporal column for dashboard time filters (#41895) 2026-07-10 16:48:09 -04:00
Beto Dealmeida
5b8e94de26 feat(semantic layers): dashboard filters (#40475) 2026-07-10 12:18:51 -07:00
Evan Rusackas
a6ce2fd76b fix(mcp): patch list_metrics DAOs via module object to avoid Python 3.10 mock resolution bug (#41881)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 07:29:29 -07:00
Ville Brofeldt
2112fbd379 feat(subjects): add read-only Subject REST API and DAO (#41897) 2026-07-10 04:29:34 -07:00
Mehmet Salih Yavuz
1631d52b65 fix(CollectionControl): a11y drag handle and vertical axis lock (#39863)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-10 11:24:25 +02:00
dependabot[bot]
1ddc3e45d7 chore(deps): bump @deck.gl/mapbox from 9.3.5 to 9.3.6 in /superset-frontend (#41926)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 02:16:39 -07:00
dependabot[bot]
2a3d65f61a chore(deps): bump docker/login-action from 4.3.0 to 4.4.0 (#41927)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 02:16:35 -07:00
dependabot[bot]
169c05f8ce chore(deps-dev): bump tsx from 4.22.4 to 4.22.5 in /superset-frontend (#41928)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 02:16:31 -07:00
dependabot[bot]
38707866c0 chore(deps-dev): bump baseline-browser-mapping from 2.10.40 to 2.10.41 in /superset-frontend (#41929)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 02:16:27 -07:00
Joe Li
e2ba378b69 fix(sqllab): redirect legacy /superset/sql/<db_id>/ deep links (#41759)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 01:16:59 -07:00
schoel-bis
597c9c6fea fix: do not require recaptcha public key for LDAP authentication (#37009)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 01:07:01 -07:00
Evan Rusackas
be600f78fc chore(deps): upgrade ag-grid to 36.0.0 (#41882)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:33:03 +07:00
dependabot[bot]
ea76d0d785 chore(deps): bump cookie from 1.1.1 to 2.0.1 in /superset-websocket (#41874)
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-10 12:31:58 +07:00
Evan Rusackas
0ff52d46e0 fix(docs): correct broken In the Wild logo for Région Hauts-de-France (#41918)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 10:14:08 +07:00
Mike Bridge
a899e1db41 feat(versioning): entity-version base infrastructure (gated off, dark launch) (#41176)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:57:05 -07:00
Evan Rusackas
1bfb7c60e7 fix(ci): repair stale mock target breaking Python-Unit on master (#41911) 2026-07-10 09:31:50 +07:00
carlosribeiro-plaid
2380d576d5 fix(cli): avoid DetachedInstanceError in compute-thumbnails (#41530)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-09 19:26:41 -07:00
abderrahmen bejaoui
43bfc85941 fix(db): validate schema and table cache timeout to reject negative values (#38490)
Co-authored-by: abderbj <115119179+abderbj@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-09 18:19:00 -07:00
Rahul Shelke
bea5f8d4a4 fix(dashboard): stagger manual refresh to prevent worker saturation (#40512)
Signed-off-by: shelkesays <674591+shelkesays@users.noreply.github.com>
2026-07-09 17:53:04 -07:00
Greg Neighbors
baff395d98 feat(mcp): add list_themes, get_theme_info, and create_theme tools (#41497)
Co-authored-by: Greg Neighbors <gregneighbors@Gregs-Air-2.lan>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 17:51:51 -07:00
Gabriel Torres Ruiz
1fd43ffe52 feat(mcp): scope embedded-guest data reads to the token's dashboards (#41753) 2026-07-09 14:58:01 -03:00
dependabot[bot]
afc7dbeff2 chore(deps-dev): update teradatasql requirement from >=16.20.0.23 to >=20.0.0.62 (#41886)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 10:17:57 -07:00
dependabot[bot]
c1d4401d29 chore(deps): bump github/codeql-action/init from 4.36.2 to 4.36.3 (#41899)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 10:17:45 -07:00
dependabot[bot]
a4418a27c5 chore(deps): bump github/codeql-action/analyze from 4.36.2 to 4.36.3 (#41900)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 09:34:41 -07:00
dependabot[bot]
30e1ab3203 chore(deps): bump docker/login-action from 4.2.0 to 4.3.0 (#41901)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 09:34:37 -07:00
dependabot[bot]
3dd5e69714 chore(deps-dev): bump @types/node from 26.0.1 to 26.1.0 in /superset-websocket (#41887)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 12:56:24 +07:00
Durgaprasad M L
227b3a48c1 fix(chart-data): return 403 for SupersetSecurityException in chart data API (#41865) 2026-07-09 08:21:36 +07:00
madhushreeag
74c3f3b66b feat(theme): make default theme changeable through config.py (#41007)
Co-authored-by: madhushree agarwal <madhushree_agarwal@apple.com>
2026-07-08 15:38:21 -07:00
madhushreeag
ea5950558b feat(dashboard): add vertical alignment guides and height indicator when resizing charts (#41079)
Co-authored-by: madhushree agarwal <madhushree_agarwal@apple.com>
2026-07-08 15:02:56 -07:00
dependabot[bot]
3a10f39c20 chore(deps-dev): bump pytest from 7.4.4 to 9.1.1 (#41888)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 13:48:31 -07:00
dependabot[bot]
a0d9e82c60 chore(deps): bump celery from 5.5.2 to 5.6.3 (#41892)
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-08 13:16:30 -07:00
dependabot[bot]
15f8b8e9b4 chore(deps): bump immer from 11.1.8 to 11.1.9 in /superset-frontend (#41894)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-08 12:26:28 -07:00
dependabot[bot]
31351eb52a chore(deps-dev): bump @types/node from 26.0.1 to 26.1.0 in /superset-frontend (#41896)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-08 12:26:10 -07:00
dependabot[bot]
8e549e4975 chore(deps-dev): update playwright requirement from <2,>=1.60.0 to >=1.61.0,<2 (#41890)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-08 12:11:45 -07:00
Kevin Lan
e65b2c49d7 fix(marshmallow): add compatibility layer for Flask-AppBuilder with marshmallow 4.x (#35920)
Co-authored-by: FLAME <xaustin03@gmail.com>
Co-authored-by: Eyang0612 <eddieyang0612@gmail.com>
2026-07-08 12:09:50 -07:00
Đỗ Trọng Hải
2c8cdbbd9b chore(dependabot): unify npm Dependabot config used for multi-workspace superset-frontend (#41586) 2026-07-09 01:15:15 +07:00
Đỗ Trọng Hải
83716cb8c0 build(websocket): migrate to ESM (#41680) 2026-07-09 01:14:44 +07:00
Ville Brofeldt
33f0fc93ed feat: introduce Subject model and entity editors/viewers (#38831) 2026-07-08 11:00:03 -07:00
Evan Rusackas
01e872c105 refactor: remove the deprecated AVOID_COLORS_COLLISION feature flag (#41800)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-08 09:48:32 -07:00
Daniel
34d4966f8b fix(dataset): resolve duplicate virtual dataset validation error (#37932)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 08:27:13 -07:00
Beto Dealmeida
001d6c9692 fix(semantic-layers): apply time-comparison offset to TEMPORAL_RANGE filters (#41556) 2026-07-08 08:25:42 -07:00
Evan Rusackas
e333135a0d feat(i18n): expose the six newly backfilled locales (#41827)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-07-08 08:25:14 -07:00
Jean Pommier
dc5c5af467 docs: Add geOrchestra project and pi-geosolutions to In the Wild (#41801) 2026-07-08 08:24:03 -07:00
Evan Rusackas
deefe897f1 chore(frontend): remove orphaned eslint-plugin-cypress devDependency (#41866)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:52:54 +07:00
Elizabeth Thompson
49ed206381 fix(a11y): add aria-label to dependency-list remove icon (#41878) 2026-07-08 21:36:52 +07:00
Alexandru Soare
8a599d70b4 fix(explore): admin creating new chart crashes on save (#41880) 2026-07-08 16:57:17 +03:00
Enzo Martellucci
d81b135a04 fix(explore): render DragOverlay so dragged metric/column is visible (#41879) 2026-07-08 14:49:45 +02:00
dependabot[bot]
211bcbc635 chore(deps-dev): bump @swc/plugin-emotion from 14.14.0 to 14.14.1 in /superset-frontend (#41875)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-08 04:40:28 -07:00
Alexandru Soare
3b1abf8260 feat(Icons): adding Layout icon (#41853) 2026-07-08 12:25:51 +03:00
yousoph
01cd215202 fix(explore): default Save As for new charts in save dialog (#41314)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-08 00:44:34 -07:00
jaehoon
2dab31c130 fix(mcp): serialize FAB role permissions (#41404)
Co-authored-by: jaehoon.kim <jaehoon.kim@42dot.ai>
2026-07-07 23:27:30 -07:00
Paul Spitzner
f74de0eb6c fix: Show metric in the default tooltip of deck.gl GeoJson Polygon chart (#41341)
Co-authored-by: Damian Pendrak <dpendrak@gmail.com>
2026-07-07 23:22:15 -07:00
Shaitan
206155f986 fix(explore): apply per-datasource access check on the legacy explore view (#41425)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:08:17 -07:00
innovark
f6d03e4a68 fix(clickhouse): time filters use server timezone for toDateTime literals (#41579) 2026-07-07 23:07:46 -07:00
Maxime Beauchemin
0efcd54250 fix(charts): fix time comparison crash when offsets share a numeric prefix (#39344)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-07 17:46:30 -07:00
Jacobdavis12
3fa000ba0b fix(chart): allow custom date format for week range in Big Number with Trendline (#35752) 2026-07-07 17:46:06 -07:00
Akshat Sinha
9617155289 fix(utils): urls preserve query params (#35936) 2026-07-07 17:11:19 -07:00
Nicolas
e49945fc2f fix(table charts v1 & v2): pagination, translation and search by (#36881)
Co-authored-by: Nicolas <n.berry@urios.com>
2026-07-07 17:10:44 -07:00
JUST.in DO IT
866f8c51ee fix(sqllab): invalid height in ResultTable (#37408) 2026-07-07 17:09:45 -07:00
abdullah reveha
98fc857613 feat(cache): warm up native filter option queries (#41531)
Co-authored-by: Abdullah Sahin <you@example.comclear>
2026-07-07 17:08:40 -07:00
Jay Masiwal
ddca50c820 fix(ui): improve getPopupContainer to avoid double scrollbar without breaking modal behavior (#38792) 2026-07-07 17:04:31 -07:00
mkramer5454
c253b68d7b fix(chart): fix Chart Time Grain option not working if Dataset is not saved (#38766)
Co-authored-by: Matthew Kramer <matthewkramer@Matthews-MacBook-Pro.local>
Co-authored-by: Matthew Kramer <matthewkramer@Matthews-MacBook-Pro-2.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:00:40 -07:00
Jeremy
050acc6c30 fix(databend): support configuring Databend via individual parameters (#41513) 2026-07-07 16:58:33 -07:00
abdullah reveha
7b95d8b17c feat(alerts): add user lookup for email recipients (#41338)
Co-authored-by: Abdullah Sahin <you@example.comclear>
2026-07-07 16:57:35 -07:00
innovark
0d9533fdf8 fix(reports): export server-paginated table row limits (#41103) 2026-07-07 16:57:11 -07:00
Open Loop
6b1311a6c7 fix(sqllab): correct verticalalign CSS typo in SqlEditorTabHeader icon styles (#41052)
Signed-off-by: albraa911sa-lgtm <albraa911sa@gmail.com>
2026-07-07 16:48:31 -07:00
dependabot[bot]
f1c0a461fd chore(deps): bump react-markdown from 9.0.7 to 10.1.0 in /superset-frontend/packages/superset-ui-core (#41805)
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-07 16:46:07 -07:00
SBIN2010
ef10501e6b feat(table v2): add tooltip to table header (#39287) 2026-07-07 16:44:50 -07:00
jesperct
17ae25b8d4 fix(explore): resolve adhoc Custom SQL dimension in Bubble chart (#41861) 2026-07-07 16:44:24 -07:00
Đỗ Trọng Hải
2a77c0156a fix(frontend/setup): sanitize returned client error message when shown as HTML content (#41768)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-07 16:44:04 -07:00
goingforstudying-ctrl
8f4c411765 fix(mcp): coerce Role ORM objects to role names in UserInfo schema (#40746)
Signed-off-by: goingforstudying-ctrl <goingforstudying-ctrl@users.noreply.github.com>
Co-authored-by: goingforstudying-ctrl <goingforstudying@users.noreply.github.com>
Co-authored-by: goingforstudying-ctrl <goingforstudying-ctrl@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:35:27 -07:00
dependabot[bot]
0936160633 chore(deps-dev): bump eslint-plugin-cypress from 3.6.0 to 6.4.2 in /superset-frontend (#41849)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 15:14:33 -07:00
Elizabeth Thompson
d9e1f34527 fix(a11y): add aria-label to notification method remove button (#41850) 2026-07-07 15:01:30 -07:00
dependabot[bot]
4038c9d1e6 chore(deps-dev): bump google-cloud-bigquery from 3.27.0 to 3.42.1 (#41807)
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-07 14:13:28 -07:00
dependabot[bot]
8108496e2a chore(deps-dev): bump @types/d3-scale from 2.2.10 to 4.0.9 in /superset-frontend/packages/superset-ui-core (#41318)
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>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-07-07 11:36:45 -07:00
yousoph
098b95560d fix(oauth2): retrigger queries for unsaved charts and schema loading after OAuth2 redirect (#41101) 2026-07-07 11:33:11 -07:00
dependabot[bot]
6edbf425f7 chore(deps-dev): update sqlalchemy-ocient requirement from >=1.0.0 to >=3.0.0 (#41806)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:28:13 -07:00
dependabot[bot]
8e98ae701b chore(deps): bump cryptography from 48.0.1 to 49.0.0 (#41811)
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-07 11:07:52 -07:00
dependabot[bot]
054be4d45e chore(deps-dev): bump eslint-plugin-storybook from 10.4.5 to 10.4.6 in /superset-frontend (#41360)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 10:24:46 -07:00
Amin Ghadersohi
fbc24959ab feat(mcp): add semantic layer MCP tools (list_metrics, get_table, get_compatible_dimensions, get_compatible_metrics) (#41611) 2026-07-07 12:51:35 -04:00
Jean Massucatto
fe7d9b4724 fix(sqllab): reflect query history deletion without page refresh (#41019)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:40:33 -07:00
Evan Rusackas
5f27f88e73 test(preset-chart-deckgl): prove legend clicks no longer scroll to top (#41831)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 09:34:14 -07:00
Mike Bridge
04f8b700d7 feat(datasets): soft-delete and restore (#40130)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 08:57:08 -07:00
Evan Rusackas
a01a4ae511 test(superset-ui-core): prove angle-bracketed non-HTML values render as text (#34082) (#41829)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:55:39 -07:00
Evan Rusackas
0d9b3f3b05 test(dashboard): prove filters with defaults apply without touching every filter (#41841)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:55:20 -07:00
Evan Rusackas
51a171f821 test(dashboard): prove BigInt column values no longer break native filter config (#41840)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:55:00 -07:00
Evan Rusackas
19c982d8d3 test(async-queries): prove OAuth2RedirectError survives async chart queries (#41839)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:54:40 -07:00
Evan Rusackas
973366d30b test(models): prove owner association tables reject duplicate rows (#41837)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:54:08 -07:00
Evan Rusackas
7b0969131f test(sql): prove ClickHouse parametric aggregates parse cleanly (#37285) (#41836)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:53:49 -07:00
Evan Rusackas
00a9546de9 test(sql): prove multi-space column names no longer trip subquery validation (#41835)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:53:20 -07:00
Evan Rusackas
de5a31a2cf test(sql): prove Oracle GROUP BY is no longer rewritten to ordinals (#35414) (#41834)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:53:05 -07:00
Evan Rusackas
af058ccecb test(plugin-chart-echarts): prove user-selected plain legend type is honored (#39540) (#41833)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:52:49 -07:00
Evan Rusackas
d0b42d8c19 test(dashboard): prove activeTabs from permalinks selects tab content (#41832)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:52:33 -07:00
Evan Rusackas
f0e25fb978 test(preset-chart-deckgl): prove ctrl+click on deck.gl legend toggles the layer without opening a new tab (#41830)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:51:53 -07:00
dependabot[bot]
77b98a5d67 chore(deps-dev): update databend-sqlalchemy requirement from <1.0,>=0.3.2 to >=0.5.5,<1.0 (#41479)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 08:42:20 -07:00
Rafael Benitez
dd0277e1e3 feat(mcp): support embedded guest-token authentication (#41003)
Co-authored-by: Gabriel Torres Ruiz <gabo2595@gmail.com>
2026-07-07 12:24:02 -03:00
amaannawab923
9b508ebe0d fix(ag-grid): honor dataset hour offset in time range filters (#41391)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:12:01 +02:00
dependabot[bot]
53c3b1ceb1 chore(deps): bump fs-extra from 11.3.5 to 11.3.6 in /superset-frontend (#41847)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 06:38:21 -07:00
dependabot[bot]
0dcb09b163 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.62.0 to 8.62.1 in /superset-frontend (#41822)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 06:36:55 -07:00
dependabot[bot]
3a514dc8eb chore(deps-dev): update sqlalchemy-singlestoredb requirement from <2,>=1.1.1 to >=1.2.1,<2 (#41809)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 06:36:51 -07:00
dependabot[bot]
aa330422f1 chore(deps-dev): bump prettier from 3.9.3 to 3.9.4 in /superset-websocket (#41844)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 06:36:47 -07:00
dependabot[bot]
7bdde4a36a chore(deps-dev): bump prettier from 3.9.3 to 3.9.4 in /superset-frontend (#41846)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 06:36:43 -07:00
dependabot[bot]
5e16c4eaaf chore(deps-dev): bump oxlint from 1.71.0 to 1.72.0 in /superset-frontend (#41848)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 06:36:37 -07:00
amaannawab923
ffd0982942 fix(ag-grid): keep basic conditional formatting aligned after sort (#41390)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 15:12:44 +02:00
Beto Dealmeida
4dc45aa958 fix(semantic layers): apply post-processing (#41601) 2026-07-07 08:52:44 -04:00
Beto Dealmeida
4167a21f7d fix(semantic layers): guards for zero rows (#41600) 2026-07-07 08:51:45 -04:00
amaannawab923
5e384f54d3 fix(ag-grid): select cell on click instead of its text (#41392)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com>
2026-07-07 10:35:13 +02:00
anamitraadhikari
ec4e5deb01 feat(theming): add opt-in SQL Lab grid tokens and fix modal border-radius clipping (#41031)
Co-authored-by: aadhikari <aadhikari@apple.com>
2026-07-07 09:54:58 +02:00
Mehmet Salih Yavuz
de5a233ccf fix(sqllab): preserve saved query description when editing (#41685) 2026-07-07 09:19:25 +02:00
MouhibKhammassi
c846da8dfb fix(frontend): resolve image download formatting and cleanup issues (#38927)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:54:26 -07:00
Evan Rusackas
1c3b070d34 feat(i18n): do-not-translate registry + '#. do-not-translate' marker standard (#41651)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:31:41 -07:00
dependabot[bot]
a8a1b220fd chore(deps): update @ant-design/icons requirement from ^6.2.5 to ^6.3.2 in /superset-frontend/packages/superset-ui-core (#41790)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:31:19 -07:00
Evan Rusackas
e52240b9a6 test(datasets): prove column metadata persists for zero-row queries (#41838)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-07 08:49:18 +07:00
dependabot[bot]
87fa7e5e64 chore(deps-dev): bump prettier from 3.9.1 to 3.9.3 in /superset-websocket (#41810)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 08:45:20 +07:00
dependabot[bot]
2b74e140f3 chore(deps-dev): update sqlalchemy-parseable requirement from <0.2.0,>=0.1.3 to >=0.1.6,<0.2.0 (#41814)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 08:44:49 +07:00
dependabot[bot]
847bdad960 chore(deps-dev): bump @typescript-eslint/parser from 8.62.0 to 8.62.1 in /superset-frontend (#41820)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 08:42:44 +07:00
dependabot[bot]
4b78758fd2 chore(deps-dev): bump typescript-eslint from 8.62.0 to 8.62.1 in /superset-websocket (#41815)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 08:42:15 +07:00
831 changed files with 62507 additions and 21286 deletions

289
.github/dependabot.yml vendored
View File

@@ -46,7 +46,6 @@ updates:
cooldown:
default-days: 7
- package-ecosystem: "pip"
directory: "/"
open-pull-requests-limit: 10
@@ -103,291 +102,3 @@ updates:
versioning-strategy: increase
cooldown:
default-days: 7
# Now for all of our plugins and packages!
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-calendar/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-partition/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-world-map/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-pivot-table/"
ignore:
# TODO: remove below entries until React >= 19.0.0
- dependency-name: "react-icons"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-chord/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-horizon/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-rose/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-preset-chart-deckgl/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-table/"
ignore:
# TODO: remove below entries until React >= 19.0.0
- dependency-name: "react-icons"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-country-map/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-map-box/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-preset-chart-nvd3/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-word-cloud/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-echarts/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-ag-grid-table/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-cartodiagram/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-handlebars/"
ignore:
# `just-handlerbars-helpers` library in plugin-chart-handlebars requires `currencyformatter`` to be < 2
- dependency-name: "currencyformatter.js"
update-types: ["version-update:semver-major"]
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/generator-superset/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-chart-controls/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-core/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-switchboard/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7

View File

@@ -46,7 +46,7 @@ jobs:
- name: Login to Docker Hub
if: steps.check.outputs.python
continue-on-error: true
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -64,7 +64,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -75,6 +75,6 @@ jobs:
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
category: "/language:${{matrix.language}}"

View File

@@ -38,7 +38,7 @@ jobs:
- name: Login to Docker Hub
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
continue-on-error: true
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -63,7 +63,7 @@ repos:
hooks:
- id: prettier-frontend
name: prettier (frontend)
entry: bash -c 'cd superset-frontend && for file in "$@"; do npx prettier --write "${file#superset-frontend/}"; done'
entry: bash -c 'cd superset-frontend && files=(); for f in "$@"; do files+=("${f#superset-frontend/}"); done; npx prettier --write -- "${files[@]}"' --
language: system
pass_filenames: true
files: ^superset-frontend/.*\.(js|jsx|ts|tsx|css|scss|sass|json)$

View File

@@ -619,14 +619,34 @@ categories:
- name: Symmetrics
url: https://www.symmetrics.fyi
Government:
Government / Public authority:
- name: Centre Régional Auvergnat de l'Information Géographique (France)
url: https://www.craig.fr/
logo: craig.png
- name: City of Ann Arbor, MI
url: https://www.a2gov.org/
contributors: ["@sfirke"]
- name: Métropole Européenne de Lille (France)
url: https://data.lillemetropole.fr/accueil/
logo: mel.jpg
- name: NRLM - Sarathi, India
url: https://pib.gov.in/PressReleasePage.aspx?PRID=1999586
- name: Région Bretagne (France)
url: https://geobretagne.fr/
logo: geobretagne.svg
- name: Région Hauts-de-France (France)
url: https://www.geo2france.fr/
logo: geo2france.png
- name: Rennes Métropole (France)
url: https://metropole.rennes.fr/
logo: rennes-ville-metropole_noir.svg
- name: RIS3 Strategy of CZ, MIT CR
url: https://www.ris3.cz/
contributors: ["@RIS3CZ"]
@@ -682,6 +702,10 @@ categories:
url: https://automattic.com/
contributors: ["@Khrol", "@Usiel"]
- name: CamptoCamp
url: https://camptocamp.com/
logo: camptocamp.svg
- name: Dropbox
url: https://www.dropbox.com/
contributors: ["@bkyryliuk"]
@@ -690,6 +714,10 @@ categories:
url: https://flowbird.com
contributors: ["@EmmanuelCbd"]
- name: geOrchestra
url: https://www.georchestra.org
logo: georchestra.svg
- name: GEOTAB
url: https://www.geotab.com
contributors: ["@JZ6"]
@@ -706,6 +734,10 @@ categories:
url: https://www.increff.com/
contributors: ["@ishansinghania"]
- name: jDev
url: https://jdev.fr/
logo: jDev.svg
- name: Let's Roam
url: https://www.letsroam.com/
@@ -716,6 +748,11 @@ categories:
url: https://1beat.com/
contributors: ["@GuyAttia"]
- name: pi-Geosolutions
url: https://www.pi-geosolutions.fr/
logo: pi-Geosolutions.png
contributors: ["@jeanpommier"]
- name: X
url: https://x.com/

View File

@@ -24,6 +24,94 @@ assists people when migrating to a new version.
## Next
### Owners, dashboard roles, and RLS roles replaced by Subjects
Superset now uses subject-based access assignments for dashboards, charts, datasets,
alerts/reports, and Row Level Security. A Subject can represent a user, role, or group.
This is a breaking API and metadata change:
- `owners` is replaced by `editors` for dashboards, charts, datasets, and alerts/reports.
- Dashboard `roles` and the `DASHBOARD_RBAC` feature flag are replaced by dashboard/chart
`viewers`, enabled with `ENABLE_VIEWERS`.
- RLS `roles` is replaced by `subjects`.
- The legacy `dashboard_user`, `slice_user`, `sqlatable_user`, `report_schedule_user`,
`dashboard_roles`, and `rls_filter_roles` tables are migrated into subject junction tables
and dropped on upgrade.
For deployments that previously used `DASHBOARD_RBAC` and later disabled it: remove stale rows
from the legacy `dashboard_roles` table before upgrading, otherwise those role assignments will
become active dashboard Viewers after migration.
API clients and automation should send and read `editors`, `viewers`, and `subjects` instead
of the legacy fields.
Subject pickers support users, groups, and roles, but only users and groups are selectable by
default. Roles remain supported as Subject types for backwards compatibility with RLS role
assignments and the previous `DASHBOARD_RBAC` model, but they are not recommended for new
resource-specific assignments. Prefer groups for membership-based access and keep roles focused
on capability grants. Existing Role subject assignments remain effective after migration even when
Roles are hidden from the default dropdown values; configure the relevant `SUBJECTS_RELATED_TYPES_*`
setting to make Roles selectable when editing subject lists. See the [Security documentation](docs/admin_docs/security/security.mdx#subjects)
for the full Subject model and picker configuration guidance.
To make roles selectable everywhere:
```python
from superset.subjects.types import SubjectType
SUBJECTS_RELATED_TYPES = [
SubjectType.USER,
SubjectType.GROUP,
SubjectType.ROLE,
]
```
To make roles selectable for RLS while other pickers keep the user and group default, use the
RLS-specific override:
```python
from superset.subjects.types import SubjectType
SUBJECTS_RELATED_TYPES_RLS = [
SubjectType.USER,
SubjectType.GROUP,
SubjectType.ROLE,
]
```
Entity-specific `SUBJECTS_RELATED_TYPES_*` settings replace `SUBJECTS_RELATED_TYPES` for that
picker.
Deployments using `EXTRA_OWNERS_RESOLVER` must migrate to `EXTRA_EDITORS_RESOLVER`. The new
resolver should return editor Subjects, subject IDs, or dicts with an `id` key instead of FAB
User objects. API responses expose these dynamic assignments as `extra_editors` instead of
`extra_owners`.
`DASHBOARD_RBAC` has been removed. To preserve the previous Dashboard RBAC behavior, enable both
subject viewers and viewer datasource bypass:
```python
FEATURE_FLAGS = {
"ENABLE_VIEWERS": True,
}
VIEWER_PROMISCUOUS_MODE = True
```
Enabling only `ENABLE_VIEWERS` allows assigning dashboard/chart viewer subjects, but viewers still
need normal datasource permissions unless `VIEWER_PROMISCUOUS_MODE` is also enabled.
For backwards compatibility, enabling `ENABLE_VIEWERS` does not change access for dashboards or
charts that have no assigned viewers. Those resources continue to use the implicit dataset-access
model: users who can access the underlying dataset can still see published dashboards that use that
dataset and charts backed by that dataset.
Assigning one or more viewers opts that resource into explicit viewer access for non-editors. To
return a resource to the implicit dataset-access model, remove all viewers from it. Explicit Viewers
are the intended model going forward; deprecating and removing implicit viewership can be considered
in a later major version.
- [41044](https://github.com/apache/superset/issues/41044): Removes the deprecated `AVOID_COLORS_COLLISION` feature flag (it defaulted to `True`). Color-collision avoidance is now permanently enabled; any config override setting it to `False` is ignored.
- [39925](https://github.com/apache/superset/pull/39925): URL prefixing for `SUPERSET_APP_ROOT` subdirectory deployments is now handled automatically by helpers in `src/utils/navigationUtils` (`openInNewTab`, `redirect`, `getShareableUrl`, `<AppLink>`). Direct imports of `ensureAppRoot` / `makeUrl` from `src/utils/pathUtils` are forbidden outside `navigationUtils.ts` (enforced by a static-invariant test); contributors writing new code should use the focused helpers instead. No runtime behaviour change for existing callers — all 19 prior call sites have been migrated and four pre-existing double-prefix and missing-prefix bugs are fixed as part of the migration.
- [39925](https://github.com/apache/superset/pull/39925): `SupersetClient.getUrl()` now strips a single leading application-root segment from the supplied `endpoint` before building the request URL, so a caller that accidentally pre-prefixes its endpoint (for example by wrapping it with `ensureAppRoot` before passing it to the client) no longer produces a doubled `/superset/superset/...` URL under subdirectory deployment. The strip is **single-pass** — a genuine `/superset/superset/<slug>` route is preserved, not collapsed — and **silent** (no console warning); the static-invariant test remains the primary signal for pre-prefixing at the call site, and this runtime strip is a safety net beneath it. Code that intentionally targeted a literal `/<app_root>/<app_root>/...` endpoint through `getUrl` (a configuration that has no legitimate use under the prefixing model) would have its first redundant segment removed.
@@ -42,6 +130,8 @@ assists people when migrating to a new version.
- **New config flag `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE` (default `False`).** Share/permalink URLs now substitute `window.location.origin` for the backend-supplied origin so a proxied or subdirectory-deployed Superset never hands the user an unreachable internal hostname. Operators whose reverse proxy correctly forwards `X-Forwarded-Host` *and* who want permalinks to carry the backend's literal origin can opt out by setting `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True` in `superset_config.py`. Default `False` (rewrite is on); flipping the default would regress the dominant proxied/subdir deployment to an unreachable host.
- [41651](https://github.com/apache/superset/pull/41651): **New do-not-translate standard for translation catalogs.** Strings that must stay identical to the source — icon names (e.g. `bolt`), enum/option values (`step-after`), SQL keywords, API field names (`error_message`), code constants, and example placeholders — are now marked with a `#. do-not-translate` extracted comment. The list lives in the `superset/translations/do-not-translate.txt` registry; `scripts/translations/apply_do_not_translate.py` stamps the marker onto `messages.pot` during `babel_update.sh`, and `pybabel update` propagates it to every `.po`, so the status is consistent across all languages. The AI backfill (`backfill_po.py`) and translators leave these entries untranslated (source fallback). The legacy per-catalog convention (a `# Не переводить` translator comment in the `ru` catalog) is still honored for back-compat but is superseded by this standard; contributors adding new machine-read strings should add the msgid to the registry rather than annotating individual catalogs.
### SQL Lab denies large-object and information_schema access by default
`DISALLOWED_SQL_FUNCTIONS` and `DISALLOWED_SQL_TABLES` now ship with additional default entries, so SQL Lab and chart-data queries that reference them are rejected where they were previously allowed:
@@ -123,6 +213,17 @@ ALTER TABLE tagged_object DROP CONSTRAINT <constraint_name>;
-- MySQL: find names via `SHOW CREATE TABLE tagged_object;`
ALTER TABLE tagged_object DROP FOREIGN KEY <constraint_name>;
```
### Entity version-history infrastructure (gated off by default)
Introduces the schema and SQLAlchemy-Continuum wiring that captures version history for charts, dashboards, and datasets, plus read-only `GET /api/v1/{chart,dashboard,dataset}/<uuid>/versions/` endpoints. This ships **inert**: a new config flag `ENABLE_VERSIONING_CAPTURE` defaults to `False`, so no save writes any version rows and the endpoints return empty. It is an operational kill-switch (a release toggle that becomes a permanent ops switch), not a feature flag — set it to `True` to enable capture once validated. The migration is additive; existing entity `PUT` responses gain `old_version_uuid` / `new_version_uuid` body fields and an `ETag` header (both null/absent when capture is off).
A few save- and import-path internals change **unconditionally** (independent of the flag), because the versioned mappers must behave correctly whether or not capture is enabled:
- `DatasetDAO` column/metric updates move from bulk operations to per-row ORM operations, and a metadata refresh now preserves column primary keys via a natural-key (`column_name`) upsert instead of delete-and-reinsert — so charts that reference dataset columns by id keep working across a refresh (previously such references could be invalidated).
- `ImportExportMixin.reset_ownership` stamps the current user onto `created_by`/`changed_by` when a request context is present (previously left null for the column default to fill).
- `UpdateDashboardCommand` runs its body under `no_autoflush`.
These are behavior changes that take effect on upgrade regardless of `ENABLE_VERSIONING_CAPTURE`; no operator action is required.
### Webhook alerts/reports block private/internal hosts by default
@@ -302,6 +403,36 @@ Schedule the cutover in a quiet window. Runtime reads use only the single config
The migration is transactional (all-or-nothing) and idempotent — it can be safely re-run or resumed. Note that AES-GCM, unlike AES-CBC, does not support querying directly over encrypted columns; audit any code that filters on an encrypted column before switching. See the SIP at `docs/sip/authenticated-encryption-at-rest.md` for details.
### Soft delete and restore for datasets
**The soft-delete behavior in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/dataset/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
**Flag-toggle caveat:** the soft-delete visibility filter is evaluated per query while the flag is on. If datasets are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live datasets in all lists, lookups, and relationship loads (including charts that reference them). The `POST /<uuid>/restore` endpoint and the `dataset_deleted_state` list filter remain functional regardless of the flag, deliberately, so rows soft-deleted during a flag-on window stay discoverable and restorable after a rollback of the flag.
**Flag-independent parts of this work** (active even with `SOFT_DELETE` off): the restore endpoint and deleted-state filter (above); the database-deletion guard counting soft-deleted datasets; the `get_or_create_dataset` soft-deleted-twin pre-check; the combined datasource listing (`GET /api/v1/datasource/...`) always excluding soft-deleted datasets; and the two uniqueness-validation changes documented at the end of this section. Everything else — the soft DELETE itself and the visibility filtering — is flag-gated.
With the flag enabled: `DELETE /api/v1/dataset/<id>` no longer hard-deletes the dataset (the bulk-delete endpoint behaves the same way). The row is marked with a `deleted_at` timestamp and hidden from all list, detail, and lookup endpoints. Datasets in this state are excluded from default queries and from relationship loads (e.g. `database.tables`).
**No cascade in v1.** Soft-delete does not propagate to dependent charts or dashboards: they remain visible. Loading a chart whose dataset is soft-deleted surfaces a "datasource not found" error at chart-load time. Restore the dataset to recover.
**Database deletion is blocked by soft-deleted datasets.** Superset already refuses to delete a database that still has datasets (`DatabaseDeleteDatasetsExistFailedError`); that check now explicitly counts soft-deleted datasets too (it bypasses the visibility filter), since the soft-deleted `tables` rows still reference the database via `database_id` and must not be orphaned. Consequence: because dataset `DELETE` is soft and v1 ships no hard-delete/purge, **a database that has ever had datasets cannot be deleted through the API once those datasets are soft-deleted** — the rows remain and keep blocking the delete. Until a purge capability lands, operators who must remove such a database have to hard-delete the underlying `tables` rows out-of-band first. This is a deliberate trade-off (no orphaned rows / restorable datasets) and is expected to be resolved by the planned purge work.
**Side-effect change for operators.** Because the row is no longer physically deleted, FAB `ab_view_menu` / permission-view rows tied to the dataset are also preserved. Downstream automation that relied on `DELETE /api/v1/dataset/<id>` cleaning up those rows must now react to the new `POST /api/v1/dataset/<uuid>/restore` lifecycle, or call the eventual hard-delete endpoint.
**New endpoint** — `POST /api/v1/dataset/<uuid>/restore` clears `deleted_at` and returns the dataset to active state. Requires `can_write on Dataset` and editorship of the row (or admin). Soft-deleted datasets can also be surfaced in the list endpoint via the new `dataset_deleted_state` rison filter: `include` returns both live and soft-deleted rows, `only` returns just the soft-deleted ones. Any other value is ignored. For non-admin users, soft-deleted rows are limited to datasets they can edit — the same audience that can restore them.
**Permissions migration:** existing role grants of `can_write on Dataset` cover the new restore endpoint automatically; no role migration is required.
**Schema migration:** the migration adds a nullable `deleted_at` column and an index on it (`ix_tables_deleted_at`) to the `tables` table. The column add is instant; the index build runs inline (no `CONCURRENTLY`) and may briefly block writes on the `tables` table (INSERT/UPDATE/DELETE are queued while the index builds; reads are unaffected) on large Postgres deployments. MySQL InnoDB builds the index online (no blocking). Production deployments with many thousands of datasets should run this migration during a maintenance window.
**Rollback note:** if the application code is rolled back after datasets have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted datasets silently become live, active datasets with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
**SQL Lab / dataset-creation flows:** creating a dataset over a table whose dataset sits in the trash is refused. The SQL Lab "save as dataset" flow (`get_or_create_dataset`) and file uploads return a **422 naming the hidden twin and the restore endpoint**; the plain create, update, and duplicate paths currently fail with the generic "already exists" 422. In all cases the remediation is the same: restore the hidden dataset (or use a different table name). Perm-string maintenance also covers hidden rows: renaming a database rewrites `perm`/`schema_perm`/`catalog_perm` on soft-deleted datasets and their charts, so a later restore does not resurrect stale permission strings.
**Importer behavior:** importing a dataset YAML whose UUID matches an existing **soft-deleted** dataset 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 dataset imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dataset's exact UUID is an explicit request to bring it back. The restore preserves the original PK, the chart back-reference, `table_columns`, and `sql_metrics`. Non-editors get `ImportFailedError`. Callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
**Uniqueness-validation changes that apply regardless of the feature flag:** two dataset uniqueness checks were tightened alongside this work and are active even with `SOFT_DELETE` off. (1) Create/update uniqueness treats a dataset whose `catalog` is `NULL` as belonging to the database's default catalog, so a legacy twin pair (`catalog=NULL` vs. `catalog=<default>`, same database/schema/name) that older versions allowed now fails validation with "already exists" when either row is edited — resolve by renaming or removing one of the twins. (2) Duplicating a dataset now checks name collisions scoped to the target (database, catalog, schema) instead of globally by name alone: duplicates into other databases that were previously blocked are now allowed.
### Soft delete and restore for charts
**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/chart/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
@@ -312,7 +443,7 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
**Operational notes:** a report schedule whose target chart is soft-deleted now fails its runs with an explicit error ("The chart this report targets was deleted...") until the chart is restored or the report re-pointed — chart deletion is blocked while a report references the chart, but a validate/commit race or a flag toggle can still produce this state. Dashboards **preserve** their membership rows for soft-deleted charts: saving a dashboard does not sever a trashed member, and restoring the chart re-attaches it to its dashboards.
**New endpoint** — `POST /api/v1/chart/<uuid>/restore` clears `deleted_at` and returns the chart to active state. Requires `can_write on Chart` and ownership of the row (or admin). Soft-deleted charts can also be surfaced in the list endpoint via the new `chart_deleted_state` rison filter: `include` returns both live and soft-deleted rows, `only` returns just the soft-deleted ones. Any other value is ignored. For non-admin users, soft-deleted rows are limited to charts they own — the same audience that can restore them.
**New endpoint** — `POST /api/v1/chart/<uuid>/restore` clears `deleted_at` and returns the chart to active state. Requires `can_write on Chart` and editorship of the row (or admin). Soft-deleted charts can also be surfaced in the list endpoint via the new `chart_deleted_state` rison filter: `include` returns both live and soft-deleted rows, `only` returns just the soft-deleted ones. Any other value is ignored. For non-admin users, soft-deleted rows are limited to charts they can edit — the same audience that can restore them.
**Permissions migration:** existing role grants of `can_write on Chart` cover the new restore endpoint automatically; no role migration is required.
@@ -320,7 +451,7 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
**Rollback note:** if the application code is rolled back after charts have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted charts silently become live, active charts with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-owners get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
- [39914](https://github.com/apache/superset/pull/39914) `ALERT_REPORT_SLACK_V2` now defaults to `True` and the legacy Slack v1 integration (`Slack` recipient type, `files.upload` API) is deprecated for removal in the next major. Slack blocked new apps from `files.upload` in May 2024 and fully retired the method for all apps on November 12, 2025; because the v1 path sends files through `files.upload`, v1 file-bearing sends now fail at the API level — only text-only `chat_postMessage` still works via the legacy path. Grant your Slack bot the `channels:read` and `groups:read` scopes so existing `Slack` recipients can be auto-upgraded to `SlackV2` on next send. Operators who explicitly override the flag to `False`, or whose Slack bot is missing those scopes, will see deprecation warnings while text-only sends continue through the legacy path.
@@ -332,7 +463,7 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
With the flag enabled: `DELETE /api/v1/dashboard/<id>` no longer hard-deletes the dashboard (the bulk-delete endpoint behaves the same way). The row is marked with a `deleted_at` timestamp and hidden from the dashboard API's list, detail, and lookup endpoints, which return 404 for soft-deleted dashboards. The embedded-dashboard iframe URL (`/embedded/<uuid>`) keeps rendering because it reads only `embedded.allowed_domains` and `embedded.dashboard_id` (the FK column) without dereferencing the parent dashboard; the frontend's subsequent dashboard-API fetch is what sees the 404 and surfaces "dashboard not found" to the user.
**New endpoint** — `POST /api/v1/dashboard/<uuid>/restore` clears `deleted_at` and returns the dashboard to active state. Requires `can_write on Dashboard` and ownership of the row (or admin). Soft-deleted dashboards can also be surfaced in the list endpoint via the new `dashboard_deleted_state` rison filter: `include` returns both live and soft-deleted rows, `only` returns just the soft-deleted ones. Any other value is ignored. For non-admin users, soft-deleted rows are limited to dashboards they own — the same audience that can restore them.
**New endpoint** — `POST /api/v1/dashboard/<uuid>/restore` clears `deleted_at` and returns the dashboard to active state. Requires `can_write on Dashboard` and editorship of the row (or admin). Soft-deleted dashboards can also be surfaced in the list endpoint via the new `dashboard_deleted_state` rison filter: `include` returns both live and soft-deleted rows, `only` returns just the soft-deleted ones. Any other value is ignored. For non-admin users, soft-deleted rows are limited to dashboards they can edit — the same audience that can restore them.
**Permissions migration:** existing role grants of `can_write on Dashboard` cover the new restore endpoint automatically; no role migration is required.
@@ -344,7 +475,7 @@ The partial-index replacement is dialect-dependent: PostgreSQL uses a native `WH
**Slug semantics:** on PostgreSQL and MySQL 8.0.13+, the slug of a soft-deleted dashboard is **free for reuse**. A new active dashboard can claim it immediately. Restoring a soft-deleted dashboard whose slug has since been claimed returns **422 with a clean error** (`DashboardSlugConflictError`) — rename one of the dashboards and retry; the restore is not silently rejected by a database-level constraint violation.
**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, role grants, owners, tags) — including role grants that were implicitly revoked by the deletion. Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and ownership of the deleted row (or admin) — non-owners get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
**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.
### Granular Export Controls

View File

@@ -287,8 +287,10 @@ to specify on behalf of which username to render the dashboards. In general, das
are not accessible to unauthorized requests, that is why the worker needs to take over credentials
of an existing user to take a snapshot.
By default, Alerts and Reports are executed as the owner of the alert/report object. To use a fixed user account,
just change the config as follows (`admin` in this example):
By default, Alerts and Reports are executed as a physical user represented by the alert/report editors
(giving priority to the last modifier, then the creator, then the first direct user editor, then a
deterministic user from editor roles or groups). To use a fixed user account, just change the config
as follows (`admin` in this example):
```python
from superset.tasks.types import FixedExecutor

View File

@@ -139,6 +139,10 @@ from superset.tasks.types import FixedExecutor
THUMBNAIL_EXECUTORS = [FixedExecutor("admin")]
```
When using `ExecutorType.EDITOR`, thumbnails are rendered as a physical user represented by
the dashboard or chart editors. Superset prioritizes the last modifier, then the creator,
then the first direct user editor, then a deterministic user from editor roles or groups.
For this feature you will need a cache system and celery workers. All thumbnails are stored on cache
and are processed asynchronously by the workers.
@@ -179,10 +183,10 @@ WEBDRIVER_BASEURL = "https://superset.company.com"
To control which user account is used for rendering thumbnails and warming up caches, configure
`THUMBNAIL_EXECUTORS` and `CACHE_WARMUP_EXECUTORS`. Each accepts a list of executor types (which
resolve to an owner, creator, modifier, or the currently-logged-in user) and/or a `FixedExecutor`
pinned to a specific username. By default, thumbnails render as the current user
(`ExecutorType.CURRENT_USER`) and cache warmup runs as the chart/dashboard owner
(`ExecutorType.OWNER`).
resolve to an editor, creator, modifier, or the currently-logged-in user) and/or a
`FixedExecutor` pinned to a specific username. By default, thumbnails render as the current user
(`ExecutorType.CURRENT_USER`) and cache warmup uses editor-based execution
(`ExecutorType.EDITOR`) where executor-based cache warmup is used.
To force both to run as a dedicated service account (`admin` in this example):

View File

@@ -60,9 +60,9 @@ There are two approaches to making dashboards publicly accessible:
**Option 2: Dashboard-level access (selective control)**
1. Set `PUBLIC_ROLE_LIKE = "Public"` in `superset_config.py`
2. Add the `'DASHBOARD_RBAC': True` [Feature Flag](/admin-docs/configuration/feature-flags)
3. Edit each dashboard's properties and add the "Public" role
4. Only dashboards with the Public role explicitly assigned are visible to anonymous users
2. Add the `'ENABLE_VIEWERS': True` [Feature Flag](/admin-docs/configuration/feature-flags)
3. Edit each dashboard's properties and add the "Public" role subject as a viewer
4. Only published dashboards with the Public role subject explicitly assigned are visible to anonymous users
See the [Public role documentation](/admin-docs/security/#public) for more details.

View File

@@ -176,7 +176,7 @@ https://superset.apache.org/admin-docs/configuration/configuring-superset/#rotat
### **Appendix C: Secrets Register and Rotation Schedule**
`SUPERSET_SECRET_KEY` is not the only security-critical secret in a Superset deployment. Maintain an inventory of all such secrets, store each in a secrets manager (not in `superset_config.py` or version control), assign an owner, and rotate them on a defined cadence as well as after any suspected compromise.
`SUPERSET_SECRET_KEY` is not the only security-critical secret in a Superset deployment. Maintain an inventory of all such secrets, store each in a secrets manager (not in `superset_config.py` or version control), assign a responsible maintainer, and rotate them on a defined cadence as well as after any suspected compromise.
| Secret | Purpose | Risk if leaked | Suggested rotation |
|---|---|---|---|

View File

@@ -91,24 +91,24 @@ PUBLIC_ROLE_LIKE = "Public"
# PUBLIC_ROLE_LIKE = "Gamma"
```
If you prefer to manually configure the Public role's permissions (or use `DASHBOARD_RBAC` to
grant access at the dashboard level), you do not need to set `PUBLIC_ROLE_LIKE`.
If you prefer to manually configure the Public role's permissions, or use subject-based
dashboard viewer assignments, you do not need to set `PUBLIC_ROLE_LIKE`.
**Important notes:**
- **Data access is still required:** The Public role only grants UI/API permissions. You must
also grant access to specific datasets necessary to view a dashboard. As with other roles,
this can be done in two ways:
- **Data access is still required by default:** The Public role only grants UI/API permissions.
You must also grant access to specific datasets necessary to view a dashboard. As with other
roles, this can be done in two ways:
- **Without `DASHBOARD_RBAC`:** Dashboards only appear in the list and are accessible if
the user has permission to at least one of their datasets. Grant dataset access by editing
the Public role in the Superset UI (Menu Security List Roles Public) and adding the
relevant data sources. All published dashboards using those datasets become visible.
- **Dataset-based access:** Dashboards only appear in the list and are accessible if the user
has permission to at least one of their datasets. Grant dataset access by editing the Public
role in the Superset UI (Menu -> Security -> List Roles -> Public) and adding the relevant
data sources. All published dashboards using those datasets become visible.
- **With `DASHBOARD_RBAC` enabled:** Anonymous users will only see dashboards where the
"Public" role has been explicitly added in the dashboard's properties. Dataset permissions
are not required—DASHBOARD_RBAC handles the cascading permissions check. This provides
fine-grained control over which dashboards are publicly visible.
- **Subject-based dashboard access:** When `ENABLE_VIEWERS` is enabled, anonymous users can
see published dashboards where the "Public" role subject has been explicitly added as a
viewer. If `VIEWER_PROMISCUOUS_MODE` is enabled, viewer access can bypass dataset
permission checks for dashboard rendering.
- **Role synchronization:** Built-in role permissions (Admin, Alpha, Gamma, sql_lab, and Public
when `PUBLIC_ROLE_LIKE = "Public"`) are synchronized when you run `superset init`. Any manual
@@ -131,10 +131,76 @@ tables in the **Permissions** dropdown. To select the data sources you want to a
You can then confirm with users assigned to the **Gamma** role that they see the
objects (dashboards and slices) associated with the tables you just extended them.
### Subjects
A **subject** is a unified identity that can be granted access to Superset resources such as
dashboards, charts, and datasets. Subjects come in three types:
- **User** — an individual Superset user
- **Group** — a FAB group
- **Role** — a FAB role (e.g. Alpha, Gamma, or any custom role)
Subjects are used throughout Superset to assign **editors** (who can modify a resource) and
**viewers** (who can view it). For new resource-specific assignments, Superset defaults to
**Users** and **Groups** in subject pickers. Groups are the recommended way to grant access to a
set of people because they represent organizational membership, while roles remain focused on
capability grants such as "can read dashboards" or "can write charts".
Role subjects are still supported for backwards compatibility with features that previously used
roles directly, including Row Level Security role assignments and `DASHBOARD_RBAC`. Existing role
subject assignments continue to be enforced even when roles are not exposed in the default picker
configuration. However, when an administrator edits a subject list, roles are not available as new
dropdown values unless that picker has been configured to expose role subjects. Roles are not
recommended for new subject assignments, because using roles for both permissions and resource
membership couples two separate concerns.
Administrators can control which subject types are available in pickers with
`SUBJECTS_RELATED_TYPES`. By default, this setting exposes only users and groups:
```python
from superset.subjects.types import SubjectType
SUBJECTS_RELATED_TYPES = [
SubjectType.USER,
SubjectType.GROUP,
]
```
Set `SUBJECTS_RELATED_TYPES = None` to expose all subject types, including roles. To expose roles
only for a specific entity, set that entity's override:
```python
from superset.subjects.types import SubjectType
SUBJECTS_RELATED_TYPES = [
SubjectType.USER,
SubjectType.GROUP,
]
# Leave dashboard, chart, and alert/report overrides unset so they inherit
# the users + groups default. Expose roles only in the RLS subject picker for
# compatibility with existing role-based RLS workflows.
SUBJECTS_RELATED_TYPES_RLS = [
SubjectType.USER,
SubjectType.GROUP,
SubjectType.ROLE,
]
```
Available per-entity overrides are:
- `SUBJECTS_RELATED_TYPES_DASHBOARDS` for dashboard editor/viewer pickers
- `SUBJECTS_RELATED_TYPES_CHARTS` for chart editor/viewer pickers
- `SUBJECTS_RELATED_TYPES_RLS` for Row Level Security subject pickers
- `SUBJECTS_RELATED_TYPES_ALERT_REPORTS` for alert/report editor pickers
When an override is set, it replaces `SUBJECTS_RELATED_TYPES` for that picker. When it is `None`,
the picker inherits the global default.
### Dashboard Access Control
Access to dashboards is managed via owners (users that have edit permissions to the dashboard).
Non-owner user access can be managed in two ways. Note that dashboards must be published to be
Access to dashboards is managed via editors (subjects that have edit permissions to the dashboard).
Non-editor user access can be managed in two ways. Note that dashboards must be published to be
visible to other users.
#### Dataset-Based Access (Default)
@@ -146,30 +212,40 @@ to a role (Menu → Security → List Roles).
This is the simplest approach but provides all-or-nothing access based on dataset permissions—
if a user has access to a dataset, they can see all published dashboards using that dataset.
#### Dashboard-Level Access (DASHBOARD_RBAC)
#### Dashboard and Chart-Level Access
For fine-grained control over which dashboards specific roles can access, enable the
`DASHBOARD_RBAC` feature flag:
For fine-grained control over which dashboards and charts specific users or groups can access,
enable subject-based viewers:
```python
FEATURE_FLAGS = {
"DASHBOARD_RBAC": True,
"ENABLE_VIEWERS": True,
}
```
With this enabled, you can assign specific roles to each dashboard in its properties. Users
will only see dashboards where their role is explicitly added.
With this enabled, you can assign viewer subjects to each dashboard or chart in its properties.
Users will see resources where they are assigned directly, or indirectly through a group subject.
Role subjects can also be enabled for compatibility, but groups are recommended for new viewer
assignments.
`ENABLE_VIEWERS` can be adopted incrementally. Dashboards and charts with no assigned viewers keep
the existing implicit dataset-access behavior: users who can access the underlying dataset can still
see published dashboards that use that dataset and charts backed by that dataset. Assigning one or
more viewers changes that specific resource to explicit viewer access for non-editors. Removing all
viewers returns the resource to the dataset-based fallback. Explicit Viewers are the preferred model
going forward; the implicit fallback may be deprecated and removed in a later major version.
**Important considerations:**
- Dashboard access **bypasses** dataset-level checks—granting a role access to a dashboard
implicitly grants read access to all charts and datasets in that dashboard
- Dashboards without any assigned roles fall back to dataset-based access
- Viewer access uses normal dataset checks unless `VIEWER_PROMISCUOUS_MODE` is enabled
- With `VIEWER_PROMISCUOUS_MODE`, dashboard viewer access can bypass dataset-level checks for
charts and datasets in that dashboard
- Dashboards and charts without assigned viewers fall back to dataset-based access
- The dashboard must still be published to be visible
This feature is particularly useful for:
- Making specific dashboards public while keeping others private
- Granting access to dashboards without exposing the underlying datasets for other uses
- Creating dashboard-specific access patterns that don't align with dataset ownership
- Creating dashboard-specific access patterns that don't align with dataset permissions
### SQL Execution Security Considerations
@@ -301,16 +377,16 @@ based on the roles and permissions that were attributed.
### Row Level Security
Using Row Level Security filters (under the **Security** menu) you can create filters
that are assigned to a particular dataset, as well as a set of roles.
that are assigned to a particular dataset, as well as a set of subjects.
If you want members of the Finance team to only have access to
rows where `department = "finance"`, you could:
- Create a Row Level Security filter with that clause (`department = "finance"`)
- Then assign the clause to the **Finance** role and the dataset it applies to
- Then assign the clause to the **Finance** role subject and the dataset it applies to
The **clause** field, which can contain arbitrary text, is then added to the generated
SQL statement's WHERE clause. So you could even do something like create a filter
for the last 30 days and apply it to a specific role, with a clause
for the last 30 days and apply it to a specific subject, with a clause
like `date_field > DATE_SUB(NOW(), INTERVAL 30 DAY)`. It can also support
multiple conditions: `client_id = 6` AND `advertiser="foo"`, etc.
@@ -321,12 +397,14 @@ RLS clauses also support **Jinja templating** when `ENABLE_TEMPLATE_PROCESSING`
There are two types of RLS filters:
- **Regular** — The filter clause is applied when the querying user belongs to one of the
roles assigned to the filter. Use this to restrict what specific roles can see.
- **Base** — The filter clause is applied to **all** users _except_ those in the assigned
roles. Use this to define a default restriction that privileged roles (e.g. Admin) are
exempt from. For example, a Base filter with clause `1 = 0` and the Admin role would
hide all rows from everyone except Admin — useful as a deny-by-default baseline.
- **Regular** — The filter clause is applied when the querying user matches one of the
subjects assigned to the filter. Use this to restrict what specific users, roles, or groups
can see.
- **Base** — The filter clause is applied to **all** users _except_ those matching the assigned
subjects. Use this to define a default restriction that privileged subjects (e.g. the Admin
role subject) are exempt from. For example, a Base filter with clause `1 = 0` and the Admin
role subject would hide all rows from everyone except Admin - useful as a deny-by-default
baseline.
#### Group Keys and Filter Combination
@@ -416,21 +494,20 @@ GET /api/v1/rowlevelsecurity/
GET /api/v1/rowlevelsecurity/?q=(filters:!((col:tables,opr:rel_m_m,value:<dataset_id>)))
```
**Filter RLS rules by role:**
**Filter RLS rules by subject:**
```
GET /api/v1/rowlevelsecurity/?q=(filters:!((col:roles,opr:rel_m_m,value:<role_id>)))
GET /api/v1/rowlevelsecurity/?q=(filters:!((col:subjects,opr:rel_m_m,value:<subject_id>)))
```
**View details of a specific rule** (including clause, assigned datasets, and roles):
**View details of a specific rule** (including clause, assigned datasets, and subjects):
```
GET /api/v1/rowlevelsecurity/<id>
```
The response includes the filter's `name`, `filter_type` (Regular or Base), `clause`,
`group_key`, assigned `tables` (with id, schema, and table\_name), and assigned `roles`
(with id and name).
`group_key`, assigned `tables` (with id, schema, and table\_name), and assigned `subjects`.
:::tip Auditing RLS for virtual datasets
To find all RLS rules that could affect a particular virtual dataset, query the list

View File

@@ -26,7 +26,7 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper';
# MetadataBar
MetadataBar displays a row of metadata items (SQL info, owners, last modified, tags, dashboards, etc.) that collapse responsively based on available width.
MetadataBar displays a row of metadata items (SQL info, editors, last modified, tags, dashboards, etc.) that collapse responsively based on available width.
## Live Example
@@ -43,9 +43,9 @@ MetadataBar displays a row of metadata items (SQL info, owners, last modified, t
title: "Click to view query"
},
{
type: "owner",
type: "editor",
createdBy: "Jane Smith",
owners: [
editors: [
"John Doe",
"Mary Wilson"
],
@@ -104,9 +104,9 @@ function Demo() {
const items = [
{ type: 'sql', title: 'Click to view query' },
{
type: 'owner',
type: 'editor',
createdBy: 'Jane Smith',
owners: ['John Doe', 'Mary Wilson'],
editors: ['John Doe', 'Mary Wilson'],
createdOn: 'a week ago',
},
{
@@ -125,7 +125,7 @@ function Demo() {
```tsx live
function MinimalMetadata() {
const items = [
{ type: 'owner', createdBy: 'Admin', owners: ['Admin'], createdOn: 'yesterday' },
{ type: 'editor', createdBy: 'Admin', editors: ['Admin'], createdOn: 'yesterday' },
{ type: 'lastModified', value: '2 hours ago', modifiedBy: 'Admin' },
];
return <MetadataBar items={items} />;
@@ -138,7 +138,7 @@ function MinimalMetadata() {
function FullMetadata() {
const items = [
{ type: 'sql', title: 'SELECT * FROM ...' },
{ type: 'owner', createdBy: 'Jane Smith', owners: ['Jane Smith', 'John Doe', 'Bob Wilson'], createdOn: '2 weeks ago' },
{ type: 'editor', createdBy: 'Jane Smith', editors: ['Jane Smith', 'John Doe', 'Bob Wilson'], createdOn: '2 weeks ago' },
{ type: 'lastModified', value: '3 days ago', modifiedBy: 'John Doe' },
{ type: 'tags', values: ['production', 'finance', 'quarterly'] },
{ type: 'dashboards', title: 'Used in 12 dashboards' },
@@ -158,7 +158,7 @@ function FullMetadata() {
| `createdBy` | `string` | `"Jane Smith"` | - |
| `modifiedBy` | `string` | `"Jane Smith"` | - |
| `description` | `string` | `"To preview the list of dashboards go to More settings."` | - |
| `items` | `any` | `[{"type":"sql","title":"Click to view query"},{"type":"owner","createdBy":"Jane Smith","owners":["John Doe","Mary Wilson"],"createdOn":"a week ago"},{"type":"lastModified","value":"a week ago","modifiedBy":"Jane Smith"},{"type":"tags","values":["management","research","poc"]},{"type":"dashboards","title":"Added to 3 dashboards","description":"To preview the list of dashboards go to More settings."}]` | - |
| `items` | `any` | `[{"type":"sql","title":"Click to view query"},{"type":"editor","createdBy":"Jane Smith","editors":["John Doe","Mary Wilson"],"createdOn":"a week ago"},{"type":"lastModified","value":"a week ago","modifiedBy":"Jane Smith"},{"type":"tags","values":["management","research","poc"]},{"type":"dashboards","title":"Added to 3 dashboards","description":"To preview the list of dashboards go to More settings."}]` | Items displayed in the metadata bar. The `editor` type shows editors/creators. |
## Import

View File

@@ -355,6 +355,17 @@ translations](#applying-translations) above), so an AI-generated string is shown
in the UI as soon as it is built and deployed. Reviewers should verify each
entry and remove the `#, fuzzy` flag to promote it to a confirmed translation.
The script never touches entries that must stay literal — icon names, enum
values, SQL keywords, API field names, and example placeholders. These are
registered in `superset/translations/do-not-translate.txt`;
`scripts/translations/apply_do_not_translate.py` stamps them in `messages.pot`
with a `#. do-not-translate` extracted comment (run automatically
from `babel_update.sh`), which `pybabel update` then propagates to every
catalog. To mark a new string do-not-translate, add its msgid to the registry.
The backfill also honors that marker and any legacy do-not-translate translator
comment (e.g. the `ru` catalog's `# Не переводить`), leaving such entries
untranslated so they fall back to the source token.
#### Prerequisites
```bash

View File

@@ -128,7 +128,7 @@ from superset.dashboards.commands.create import CreateDashboardCommand
def test_create_dashboard_success():
properties = {
"dashboard_title": "Test Dashboard",
"owners": [1]
"editors": [1]
}
command = CreateDashboardCommand(properties)
@@ -275,9 +275,11 @@ def handle_validation_error(error):
from typing import List, Optional, Dict, Any
from superset.models.dashboard import Dashboard
def get_dashboards_by_owner(owner_id: int) -> List[Dashboard]:
"""Get all dashboards owned by a specific user"""
return db.session.query(Dashboard).filter_by(owner_id=owner_id).all()
def get_dashboards_by_editor(editor_id: int) -> List[Dashboard]:
"""Get all dashboards editable by a specific subject"""
return db.session.query(Dashboard).filter(
Dashboard.editors.any(id=editor_id)
).all()
def create_dashboard(properties: Dict[str, Any]) -> Optional[Dashboard]:
"""Create a new dashboard with the given properties"""

View File

@@ -153,10 +153,10 @@ class DashboardDAO:
return db.session.query(Dashboard).filter_by(slug=slug).first()
@classmethod
def find_by_owner(cls, owner_id: int) -> List[Dashboard]:
"""Find all dashboards owned by a user"""
return db.session.query(Dashboard).filter_by(
created_by_fk=owner_id
def find_by_editor(cls, editor_id: int) -> List[Dashboard]:
"""Find all dashboards editable by a subject"""
return db.session.query(Dashboard).filter(
Dashboard.editors.any(id=editor_id)
).all()
@classmethod

View File

@@ -120,7 +120,7 @@ SMTP_MAIL_FROM = "superset@localhost"
# Must match where your frontend is running
WEBDRIVER_BASEURL = "http://localhost:9000/"
ALERT_REPORTS_EXECUTE_AS = [ExecutorType.OWNER]
ALERT_REPORTS_EXECUTORS = [ExecutorType.EDITOR]
FEATURE_FLAGS = {
"ALERT_REPORTS": True,

View File

@@ -181,6 +181,23 @@ value in milliseconds in the JSON Metadata field:
Here, the entire dashboard will refresh at once if periodic refresh is on. The stagger time of 2.5
seconds is ignored.
The manual **Refresh dashboard** button can also stagger its chart requests, controlled by the
`SUPERSET_DASHBOARD_MANUAL_REFRESH_STAGGER_MS` server config in `superset_config.py`. This defaults
to `0`, which preserves the original behavior where every chart request fires at the same time when
the button is clicked. To opt in to staggering, set a positive number of milliseconds; the window
then becomes the larger of this value and the per-dashboard `stagger_time` metadata, which itself
defaults to `5000` milliseconds when it is not set explicitly. Setting `stagger_refresh` to `false`
in a dashboard's JSON Metadata also disables staggering on the manual-refresh path, so its charts
fire all at once even when the server config is positive. If the backend does not provide a value at
all, for example an older backend that predates this config or a value explicitly set to `None`, the
frontend falls back to a built-in default of `5000` milliseconds.
```python
# superset_config.py
# 0 (the default) fires every chart at once; set a positive value to stagger.
SUPERSET_DASHBOARD_MANUAL_REFRESH_STAGGER_MS = 0
```
**Why does flask fab or superset freeze/hang/not responding when started (my home directory is
NFS mounted)?**

View File

@@ -211,8 +211,8 @@ slices and dashboards of your own.
### Manage access to Dashboards
Access to dashboards is managed via owners and permissions. Non-owner access can be controlled
through dataset permissions or dashboard-level roles (using the `DASHBOARD_RBAC` feature flag).
Access to dashboards is managed via editors and permissions. Non-editor access can be controlled
through dataset permissions, or through dashboard viewers when `ENABLE_VIEWERS` is enabled.
For detailed information on configuring dashboard access, see the
[Dashboard Access Control](/admin-docs/security/#dashboard-access-control) section in the
@@ -228,7 +228,9 @@ title of your dashboard.
<img src={useBaseUrl("/img/tutorial/publish_button_dashboard.png" )} />
:::warning
Draft dashboards are only visible to the dashboard owners and admins. Published dashboards are visible to all users with access to the underlying datasets or if RBAC is enabled, to the roles that have been granted access to the dashboard.
Draft dashboards are only visible to dashboard editors and admins. Published dashboards are visible
to users with access to the underlying datasets, or, when `ENABLE_VIEWERS` is enabled, to users
who have viewer access directly or through an assigned role or group subject.
:::
### Mark a Dashboard as Favorite

View File

@@ -182,6 +182,13 @@
"description": "Allows users to add a superset:// DB that can query across databases. Experimental with potential security/performance risks. See SUPERSET_META_DB_LIMIT.",
"docs": "https://superset.apache.org/user-docs/databases/supported/superset-meta-database"
},
{
"name": "ENABLE_VIEWERS",
"default": false,
"lifecycle": "testing",
"description": "Subject-based viewer access control for dashboards and charts. When enabled, resources can have explicit viewer Subject assignments.",
"category": "security"
},
{
"name": "ESTIMATE_QUERY_COST",
"default": false,
@@ -256,14 +263,6 @@
"description": "Enables CSS Templates in Settings menu and dashboard forms",
"category": "runtime_config"
},
{
"name": "DASHBOARD_RBAC",
"default": false,
"lifecycle": "stable",
"description": "Role-based access control for dashboards",
"docs": "https://superset.apache.org/docs/using-superset/creating-your-first-dashboard",
"category": "runtime_config"
},
{
"name": "DASHBOARD_VIRTUALIZATION",
"default": true,
@@ -401,12 +400,6 @@
}
],
"deprecated": [
{
"name": "AVOID_COLORS_COLLISION",
"default": true,
"lifecycle": "deprecated",
"description": "Avoid color collisions in charts by using distinct colors"
},
{
"name": "DRILL_TO_DETAIL",
"default": true,

4
docs/static/img/logos/camptocamp.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/static/img/logos/craig.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/static/img/logos/geo2france.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

2
docs/static/img/logos/geobretagne.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.0 KiB

108
docs/static/img/logos/georchestra.svg vendored Normal file
View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="76.919907mm"
height="26.149284mm"
viewBox="0 0 76.919907 26.149284"
version="1.1"
id="svg855"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
sodipodi:docname="georchestra-logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview857"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
fit-margin-top="2"
fit-margin-right="2"
fit-margin-bottom="2"
fit-margin-left="2"
inkscape:zoom="4.0920134"
inkscape:cx="120.47859"
inkscape:cy="47.775992"
inkscape:window-width="2400"
inkscape:window-height="1271"
inkscape:window-x="2391"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs852" />
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-73.097929,-135.79112)">
<g
id="g53648"
transform="matrix(1.1048988,0,0,1.1048988,-54.141498,96.632368)">
<path
id="path164290-3-6-4"
style="fill:#85127e;fill-opacity:0.4;stroke-width:0.606168;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill"
d="m 126.99257,37.251152 a 10.023325,10.023325 0 0 0 -10.0231,10.023355 10.023325,10.023325 0 0 0 10.0231,10.023083 10.023325,10.023325 0 0 0 9.62187,-7.232846 3.0318022,3.5371027 0 0 1 -2.01469,-3.330589 3.0318022,3.5371027 0 0 1 1.70117,-3.175791 10.023325,10.023325 0 0 0 -9.30835,-6.307212 z"
inkscape:export-filename="C:\Users\Agathe\Desktop\georchestra_univers.png"
inkscape:export-xdpi="200"
inkscape:export-ydpi="200" />
<path
id="path20251-5-9-8-2"
style="fill:#85127e;fill-opacity:1;stroke-width:0.337452;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill"
d="m 138.54037,41.724815 a 4.988567,4.988567 0 0 0 -4.9887,4.988712 4.988567,4.988567 0 0 0 4.9887,4.98844 4.988567,4.988567 0 0 0 4.98844,-4.98844 4.988567,4.988567 0 0 0 -4.98844,-4.988712 z m 0,2.683748 a 2.3048444,2.3048444 0 0 1 2.30472,2.304964 2.3048444,2.3048444 0 0 1 -2.30472,2.304704 2.3048444,2.3048444 0 0 1 -2.30496,-2.304704 2.3048444,2.3048444 0 0 1 2.30496,-2.304964 z"
inkscape:export-filename="C:\Users\Agathe\Desktop\georchestra_univers.png"
inkscape:export-xdpi="200"
inkscape:export-ydpi="200" />
<g
aria-label="ge rchestra"
id="text10089-26-6-9"
style="font-size:9.62958px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto, Normal';letter-spacing:0px;fill:#85127e;stroke-width:0.601849">
<path
d="m 123.84851,51.576041 q -0.66682,0 -1.14556,-0.177819 -0.47875,-0.177819 -0.76941,-0.475324 -0.29067,-0.297505 -0.40351,-0.653142 l 1.11478,-0.270148 q 0.0752,0.140203 0.21886,0.287245 0.14362,0.150463 0.38641,0.249631 0.24621,0.102588 0.61895,0.102588 0.52662,0 0.872,-0.25305 0.34537,-0.25305 0.34537,-0.817283 v -0.995103 h -0.0615 q -0.0957,0.191498 -0.28041,0.393254 -0.18123,0.201756 -0.48216,0.33854 -0.2975,0.136784 -0.74889,0.136784 -0.60527,0 -1.09769,-0.283827 -0.489,-0.287246 -0.77967,-0.854899 -0.28724,-0.571072 -0.28724,-1.42939 0,-0.865158 0.28724,-1.460167 0.29067,-0.59843 0.78309,-0.906193 0.49242,-0.311183 1.09769,-0.311183 0.46165,0 0.75915,0.157301 0.30092,0.153882 0.47874,0.372736 0.17782,0.215434 0.27015,0.406932 h 0.0684 v -0.868577 h 1.2208 v 5.337987 q 0,0.659982 -0.32144,1.09769 -0.32144,0.441128 -0.87884,0.656563 -0.55739,0.218854 -1.26525,0.218854 z m 0.0103,-3.104992 q 0.39325,0 0.67024,-0.191498 0.27699,-0.191497 0.42061,-0.550555 0.14362,-0.359057 0.14362,-0.861738 0,-0.495841 -0.14362,-0.868577 -0.1402,-0.372736 -0.41719,-0.577911 -0.27357,-0.208596 -0.67366,-0.208596 -0.41377,0 -0.69076,0.215435 -0.27699,0.215434 -0.41719,0.59159 -0.1402,0.372736 -0.1402,0.848059 0,0.482163 0.1402,0.84464 0.14362,0.359058 0.42061,0.560814 0.28041,0.198337 0.68734,0.198337 z"
id="path1811" />
<path
d="m 129.91145,49.620032 q -0.78993,0 -1.36442,-0.328281 -0.57107,-0.3317 -0.87883,-0.936969 -0.30777,-0.608688 -0.30777,-1.43281 0,-0.810444 0.30777,-1.422552 0.31118,-0.615527 0.86857,-0.957486 0.5574,-0.345379 1.30971,-0.345379 0.48558,0 0.91645,0.157301 0.43429,0.153882 0.76599,0.478743 0.33512,0.324862 0.52662,0.827542 0.19149,0.499261 0.19149,1.19002 v 0.379575 h -4.30527 v -0.834382 h 3.11867 q -0.003,-0.355638 -0.15388,-0.632625 -0.15046,-0.280406 -0.42061,-0.441127 -0.26673,-0.160721 -0.62236,-0.160721 -0.37958,0 -0.66683,0.184658 -0.28724,0.181238 -0.44796,0.478743 -0.1573,0.294085 -0.16072,0.646303 v 0.728374 q 0,0.458226 0.16756,0.786507 0.16756,0.324862 0.46848,0.499261 0.30093,0.17098 0.70444,0.17098 0.27015,0 0.489,-0.07523 0.21885,-0.07865 0.37957,-0.229113 0.16073,-0.150462 0.2428,-0.372736 l 1.15582,0.129944 q -0.10943,0.458226 -0.41719,0.800186 -0.30435,0.33854 -0.77967,0.526617 -0.47532,0.184658 -1.08743,0.184658 z"
id="path1813" />
<path
d="m 145.2449,49.517445 v -5.252498 h 1.20027 v 0.875416 h 0.0547 q 0.14362,-0.454806 0.49242,-0.701017 0.35222,-0.24963 0.8036,-0.24963 0.10259,0 0.22912,0.01026 0.12994,0.0068 0.21543,0.02394 v 1.138725 q -0.0787,-0.02736 -0.24963,-0.04787 -0.16756,-0.02394 -0.32486,-0.02394 -0.33854,0 -0.60869,0.147043 -0.26673,0.143623 -0.42061,0.400092 -0.15388,0.25647 -0.15388,0.59159 v 3.087895 z"
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
id="path1815" />
<path
d="m 151.13685,49.620032 q -0.7865,0 -1.35074,-0.345379 -0.56081,-0.345379 -0.86516,-0.954067 -0.30092,-0.612107 -0.30092,-1.408873 0,-0.800185 0.30776,-1.412293 0.30777,-0.615527 0.86858,-0.957486 0.56423,-0.345379 1.33364,-0.345379 0.63947,0 1.13189,0.235952 0.49584,0.232532 0.78993,0.659982 0.29408,0.424029 0.33512,0.991682 h -1.18318 q -0.0718,-0.379575 -0.34196,-0.632625 -0.26673,-0.256469 -0.7147,-0.256469 -0.37957,0 -0.66682,0.205175 -0.28725,0.201756 -0.44797,0.581331 -0.1573,0.379575 -0.1573,0.909613 0,0.536876 0.1573,0.92329 0.1573,0.382995 0.44113,0.59159 0.28725,0.205176 0.67366,0.205176 0.27357,0 0.489,-0.102588 0.21886,-0.106007 0.3659,-0.304344 0.14704,-0.198336 0.20176,-0.482163 h 1.18318 q -0.0445,0.557394 -0.32829,0.988263 -0.28382,0.42745 -0.77282,0.670241 -0.48901,0.239371 -1.14899,0.239371 z"
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
id="path1817" />
<path
d="m 155.64387,46.439809 v 3.077636 h -1.2379 v -7.003331 h 1.21054 v 2.643347 h 0.0615 q 0.18466,-0.444547 0.57107,-0.701017 0.38984,-0.259889 0.99169,-0.259889 0.54713,0 0.95406,0.229113 0.40694,0.229113 0.62921,0.67024 0.22569,0.441128 0.22569,1.077173 v 3.344364 h -1.23789 v -3.152867 q 0,-0.530037 -0.27357,-0.824122 -0.27015,-0.297505 -0.75915,-0.297505 -0.32828,0 -0.58817,0.143623 -0.25647,0.140203 -0.40351,0.406932 -0.14362,0.266728 -0.14362,0.646303 z"
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
id="path1819" />
<path
d="m 162.6335,49.620032 q -0.78992,0 -1.36441,-0.328281 -0.57108,-0.3317 -0.87884,-0.936969 -0.30776,-0.608688 -0.30776,-1.43281 0,-0.810444 0.30776,-1.422552 0.31118,-0.615527 0.86858,-0.957486 0.55739,-0.345379 1.3097,-0.345379 0.48558,0 0.91645,0.157301 0.43429,0.153882 0.76599,0.478743 0.33512,0.324862 0.52662,0.827542 0.1915,0.499261 0.1915,1.19002 v 0.379575 h -4.30527 v -0.834382 h 3.11867 q -0.003,-0.355638 -0.15388,-0.632625 -0.15047,-0.280406 -0.42061,-0.441127 -0.26673,-0.160721 -0.62237,-0.160721 -0.37958,0 -0.66682,0.184658 -0.28725,0.181238 -0.44797,0.478743 -0.1573,0.294085 -0.16072,0.646303 v 0.728374 q 0,0.458226 0.16756,0.786507 0.16756,0.324862 0.46849,0.499261 0.30092,0.17098 0.70443,0.17098 0.27015,0 0.489,-0.07523 0.21886,-0.07865 0.37958,-0.229113 0.16072,-0.150462 0.24279,-0.372736 l 1.15582,0.129944 q -0.10942,0.458226 -0.41719,0.800186 -0.30434,0.33854 -0.77967,0.526617 -0.47532,0.184658 -1.08743,0.184658 z"
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
id="path1821" />
<path
d="m 170.16686,45.653302 -1.12846,0.123106 q -0.0479,-0.17098 -0.16756,-0.321442 -0.11627,-0.150462 -0.31461,-0.242792 -0.19833,-0.09233 -0.48558,-0.09233 -0.38641,0 -0.64972,0.167561 -0.25989,0.16756 -0.25647,0.434288 -0.003,0.229113 0.16756,0.372736 0.1744,0.143623 0.57449,0.235952 l 0.89593,0.191497 q 0.74548,0.160721 1.10795,0.50952 0.3659,0.348799 0.36932,0.913032 -0.003,0.495841 -0.29067,0.875416 -0.28382,0.376156 -0.78992,0.588171 -0.5061,0.212014 -1.16267,0.212014 -0.96432,0 -1.55249,-0.403512 -0.58817,-0.406932 -0.70102,-1.131886 l 1.20712,-0.116266 q 0.0821,0.355638 0.3488,0.536876 0.26673,0.181239 0.69417,0.181239 0.44113,0 0.70786,-0.181239 0.27015,-0.181238 0.27015,-0.447966 0,-0.225694 -0.1744,-0.372736 -0.17098,-0.147043 -0.53346,-0.225694 l -0.89593,-0.188077 q -0.75573,-0.157302 -1.11821,-0.530038 -0.36248,-0.376155 -0.35906,-0.950647 -0.003,-0.485582 0.26331,-0.84122 0.27015,-0.359058 0.74889,-0.553975 0.48217,-0.198336 1.11137,-0.198336 0.92329,0 1.45333,0.393253 0.53346,0.393254 0.65998,1.063495 z"
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
id="path1823" />
<path
d="m 173.925,44.264947 v 0.957486 h -3.01951 v -0.957486 z m -2.27403,-1.258411 h 1.23789 v 4.931056 q 0,0.24963 0.0752,0.382994 0.0787,0.129945 0.20518,0.177819 0.12652,0.04788 0.2804,0.04788 0.11627,0 0.21202,-0.0171 0.0992,-0.0171 0.15046,-0.03078 l 0.2086,0.967746 q -0.0992,0.0342 -0.28383,0.07523 -0.18124,0.04104 -0.44455,0.04787 -0.46506,0.01368 -0.8378,-0.140203 -0.37274,-0.157302 -0.59159,-0.485583 -0.21543,-0.328281 -0.21201,-0.820703 z"
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
id="path1825" />
<path
d="m 174.96112,49.517445 v -5.252498 h 1.20028 v 0.875416 h 0.0547 q 0.14363,-0.454806 0.49242,-0.701017 0.35222,-0.24963 0.80361,-0.24963 0.10259,0 0.22911,0.01026 0.12995,0.0068 0.21544,0.02394 v 1.138725 q -0.0787,-0.02736 -0.24963,-0.04787 -0.16756,-0.02394 -0.32487,-0.02394 -0.33853,0 -0.60868,0.147043 -0.26673,0.143623 -0.42061,0.400092 -0.15388,0.25647 -0.15388,0.59159 v 3.087895 z"
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
id="path1827" />
<path
d="m 180.19309,49.623452 q -0.49926,0 -0.89936,-0.177819 -0.39667,-0.181238 -0.6292,-0.533457 -0.22911,-0.352218 -0.22911,-0.868577 0,-0.444547 0.16414,-0.735213 0.16414,-0.290665 0.44796,-0.465065 0.28383,-0.174399 0.63947,-0.263309 0.35905,-0.09233 0.74205,-0.133364 0.46164,-0.04787 0.74889,-0.08549 0.28725,-0.04104 0.41719,-0.123105 0.13337,-0.08549 0.13337,-0.263309 v -0.02052 q 0,-0.386415 -0.22912,-0.59843 -0.22911,-0.212014 -0.65998,-0.212014 -0.45481,0 -0.72153,0.198336 -0.26331,0.198337 -0.35564,0.468485 L 178.6064,45.64646 q 0.13678,-0.478743 0.45138,-0.800185 0.3146,-0.324862 0.76941,-0.485583 0.45481,-0.16414 1.00536,-0.16414 0.37958,0 0.75573,0.08891 0.37616,0.08891 0.68734,0.294085 0.31118,0.201757 0.49926,0.550555 0.1915,0.348799 0.1915,0.871997 v 3.515344 h -1.19002 V 48.79591 h -0.041 q -0.11284,0.218854 -0.31802,0.410351 -0.20175,0.188078 -0.50952,0.304344 -0.30434,0.112847 -0.71469,0.112847 z m 0.32144,-0.909612 q 0.37274,0 0.6463,-0.147043 0.27357,-0.150462 0.42061,-0.396673 0.15047,-0.246211 0.15047,-0.536876 v -0.618947 q -0.0581,0.04787 -0.19834,0.08891 -0.13679,0.04104 -0.30776,0.07181 -0.17098,0.03078 -0.33854,0.05471 -0.16756,0.02394 -0.29067,0.04104 -0.27699,0.03761 -0.49584,0.123105 -0.21886,0.08549 -0.34538,0.239372 -0.12653,0.150462 -0.12653,0.389834 0,0.341959 0.24963,0.516358 0.24964,0.1744 0.63605,0.1744 z"
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
id="path1829" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

1
docs/static/img/logos/jDev.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

BIN
docs/static/img/logos/mel.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="178" height="34" version="1.0" viewBox="27 27 367 70"><switch><foreignObject width="1" height="1" x="0" y="0" requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"/><g><path d="M221.8 47.9c1.3 4.4 5 7.1 10.9 7.1 6 0 10.1-2.8 10.1-7.9 0-4.2-2.6-6.8-9.9-7.8-4.1-.6-5.5-1.6-5.5-3.6 0-2.2 1.8-3.2 4.5-3.2 3.3 0 5.1 1.5 5.8 4.4l4.2-1.2c-1.1-4.6-4.5-7.2-10.1-7.2-5.5 0-9.1 2.6-9.1 7.5 0 3.5 1.8 6.4 9 7.4 4.7.6 6.2 1.7 6.2 3.9 0 2.4-1.9 3.6-5.1 3.6-3.8 0-6-1.7-7.1-4.7zm-21.1-8.3c.5-4 2.9-6.8 7.1-6.8 3.9 0 6.5 2.4 6.5 6.8zm18.3 1.1c0-7.9-4.4-12.1-11.1-12.1-7.6 0-11.9 5.3-11.9 13.2 0 8 4.3 13.2 12.1 13.2 5.6 0 9.2-2.7 10.5-7l-4.3-1.4c-.9 3.2-3.5 4.1-6.2 4.1-4.9 0-7.1-3.1-7.4-7.2H219zm-44.7 13.8V35.9c2.5 2.4 9.6 12.8 12.4 18.6h4.6V29h-4.5v17.1c-3.3-5.9-7.9-12-12.8-17.1h-4.3v25.5zm-27.5 0V35.9c2.5 2.4 9.6 12.8 12.4 18.6h4.6V29h-4.5v17.1c-3.3-5.9-7.9-12-12.7-17.1h-4.3v25.5zm-27.4-14.9c.5-4 2.9-6.8 7.1-6.8 3.9 0 6.5 2.4 6.5 6.8zm18.2 1.1c0-7.9-4.4-12.1-11.1-12.1-7.6 0-11.9 5.3-11.9 13.2 0 8 4.3 13.2 12.1 13.2 5.6 0 9.2-2.7 10.5-7l-4.3-1.4c-.9 3.2-3.5 4.1-6.2 4.1-4.9 0-7.1-3.1-7.4-7.2h18.3zM101 33.2c3.6 0 5.5 1.7 5.5 4.6 0 2.6-1.6 4.4-5 4.4h-6.1v-9zm4.4 12.3c4.5-1.5 5.9-4.9 5.9-8.2 0-5.2-3.6-8.3-9.8-8.3H90.9v25.5h4.5v-8.3h5.2l1.7 2.8c3 4.9 4.2 5.7 9 5.7v-4.3c-2.5 0-3.4-.8-5.3-3.9zM383.9 71.9c2.8 0 4.8 1.8 4.8 5.1h-9.9c.3-3 2-5.1 5.1-5.1m8.8 6c0-6.1-3.4-9.5-8.8-9.5-6 0-9.4 4.1-9.4 10.3 0 6.1 3.4 10.3 9.7 10.3 4.5 0 7.2-2.1 8.2-5.6l-3.5-1.2c-.6 2-2.3 3-4.7 3-3.5 0-5.1-2.2-5.4-5.2h14v-2.1zm-21.5-15.5H367v26.2h4.2zm-17.4 22.7c-3.3 0-5.3-2.4-5.3-6.4s2.1-6.4 5.3-6.4 5.3 2.4 5.3 6.4c0 3.9-2.1 6.4-5.3 6.4m0 3.8c5.8 0 9.7-3.9 9.7-10.2s-3.9-10.2-9.7-10.2c-5.9 0-9.8 3.9-9.8 10.2.1 6.3 3.9 10.2 9.8 10.2m-22-3.7c-3.3 0-5.4-2.5-5.4-6.5s2.1-6.5 5.4-6.5 5.4 2.5 5.4 6.5c.1 4-2.1 6.5-5.4 6.5M326.4 96V85.5h.2c1.2 2.4 3.7 3.5 6.2 3.5 5 0 8.9-3.9 8.9-10.3s-3.9-10.3-8.9-10.3c-2.5 0-5.1 1.1-6.2 3.5h-.2v-3.1h-4.2V96zm-17.3-10.9c-3.3 0-5.3-2.4-5.3-6.4s2.1-6.4 5.3-6.4 5.3 2.4 5.3 6.4c.1 3.9-2 6.4-5.3 6.4m0 3.8c5.8 0 9.7-3.9 9.7-10.2s-3.9-10.2-9.7-10.2c-5.9 0-9.8 3.9-9.8 10.2.1 6.3 4 10.2 9.8 10.2m-10.8-20.3c-.5-.1-1-.1-1.5-.1-2.5 0-4.4.8-5.4 3.5h-.2v-3.2H287v19.7h4.2v-9.7c0-5.6 2.4-6.1 4.7-6.1.8 0 1.5.1 2.3.2v-4.3zm-17.6 16.2c-3 0-3.4-.9-3.4-6.3v-5.9h5.7v-3.7h-5.7v-4.8H273v4.8h-3.7v3.7h3.7v7c0 7 1.8 9.1 7 9.1 1 0 2.3 0 3.5-.1v-3.8zM259 71.9c2.8 0 4.8 1.8 4.8 5.1h-9.9c.4-3 2.1-5.1 5.1-5.1m8.9 6c0-6.1-3.4-9.5-8.8-9.5-6 0-9.4 4.1-9.4 10.3 0 6.1 3.4 10.3 9.7 10.3 4.5 0 7.2-2.1 8.2-5.6l-3.5-1.2c-.6 2-2.3 3-4.7 3-3.5 0-5.1-2.2-5.4-5.2h14v-2.1zm-3.9-17-9.1 3.2 1.1 3.1 9.1-3.1zm-20 2.2h-4.4c-2.9 5.9-5.5 11.8-7.6 18.6h-.3c-2.1-6.8-4.8-12.7-7.6-18.6h-4.4l-3.1 25.5h4.5l2-17.6c2.4 5.4 4.5 10.9 6.3 16.8h5q2.7-8.7 6.3-16.8l2 17.6h4.5zm-41.2 21.7c-3 0-3.4-.9-3.4-6.3v-5.9h5.7v-3.7h-5.7v-4.8h-4.2v4.8h-3.7v3.7h3.7v7c0 7 1.8 9.1 7 9.1 1 0 2.3 0 3.5-.1v-3.8zm-21.7-12.9c2.8 0 4.8 1.8 4.8 5.1H176c.4-3 2.1-5.1 5.1-5.1m8.9 6c0-6.1-3.4-9.5-8.8-9.5-6 0-9.4 4.1-9.4 10.3 0 6.1 3.4 10.3 9.7 10.3 4.5 0 7.2-2.1 8.2-5.6l-3.5-1.2c-.6 2-2.3 3-4.7 3-3.5 0-5.1-2.2-5.4-5.2h14v-2.1zm-37.3-6c2.8 0 4.8 1.8 4.8 5.1h-9.9c.3-3 2-5.1 5.1-5.1m8.8 6c0-6.1-3.4-9.5-8.8-9.5-6 0-9.3 4.1-9.3 10.3 0 6.1 3.4 10.3 9.7 10.3 4.5 0 7.2-2.1 8.2-5.6l-3.5-1.2c-.6 2-2.3 3-4.7 3-3.5 0-5.1-2.2-5.4-5.2h14v-2.1zm-21.9-15.5h-4.2v26.2h4.2zm-9.1 0h-4.2v26.2h4.2zm-9.4 6.4h-4.2v19.7h4.2zm-2-2.2c1.4 0 2.6-1.2 2.6-2.7 0-1.4-1.2-2.6-2.6-2.6-1.5 0-2.7 1.2-2.7 2.6s1.2 2.7 2.7 2.7m-14.3 22 9-25.5H109l-7.2 21h-.3l-7.2-21h-4.8l9 25.5zM57.4 74.6c-3.7-5.3-6.2-6.9-11.4-7.7v4.9c3.6.5 5.3 2.4 7.6 5.7 6.2 8.9 9.7 12.5 22.3 11.9v-4.7c-9.7.7-13-2.3-18.5-10.1m3.1-2.4c5.3 7.3 8.1 9.8 15.4 9.2v-4.7c-5.4.6-8-2.1-11.7-7.4-4.5-6.4-9.6-9.9-18.2-10.3v4.8c6.1.2 10.1 2.3 14.5 8.4M28.3 88.8h4.9V47.9c-1.5.5-3.4 1.1-4.9 1.8zm13.6-43.2c-1.5.2-3.4.5-4.9.9v42.2h4.9zm26 4.7c0-8.1-6.4-13.5-19-13.5-6.6 0-14.5 1.5-20.6 4v5c5.6-2.5 13.7-4.3 20.6-4.3 9.1 0 14.1 3.1 14.1 8.9 0 3.4-1.7 6.4-4.8 8.3 1.3.7 2.7 1.8 3.8 2.8 3.8-2.4 5.9-6.5 5.9-11.2m-18.4-22c-7 0-14.2 1.5-21.1 3.8v5C36 34.3 43.2 33 49.5 33c14.3 0 22.7 6.5 22.7 16.8 0 6-2.6 11.1-7.5 14.5 1 1.1 2.1 2.4 3 3.5 6.1-4.2 9.4-10.9 9.4-18-.1-13-10.6-21.5-27.6-21.5"/></g></switch></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

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

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.19.0](https://img.shields.io/badge/Version-0.19.0-informational?style=flat-square)
![Version: 0.20.0](https://img.shields.io/badge/Version-0.20.0-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -74,10 +74,42 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
|-----|------|---------|-------------|
| affinity | object | `{}` | |
| bootstrapScript | string | see `values.yaml` | Install additional packages and do any other bootstrap configuration in this script For production clusters it's recommended to build own image with this step done in CI |
| cache | object | `{"asyncQueries":{"keyPrefix":"qc-","timeout":86400},"cacheDb":null,"cacheUrl":null,"celeryDb":null,"celeryUrl":null,"defaultTimeout":86400,"driver":"","enabled":true,"host":null,"keyPrefix":"superset_","password":null,"port":null,"resultsBackendKeyPrefix":"superset_results","sentinel":null,"ssl":{"ca_certs":null,"certfile":null,"enabled":false,"keyfile":null,"ssl_cert_reqs":"required"},"user":""}` | Redis cache configuration for Superset Redis is optional but recommended for caching and Celery. If redis.enabled (chart dependency) is true, defaults point to the chart's Redis instance. |
| cache.asyncQueries | object | `{"keyPrefix":"qc-","timeout":86400}` | Async queries configuration |
| cache.cacheDb | string | `nil` | Redis database number for cache (default: 1 when unset; legacy redis_cache_db is honored) |
| cache.cacheUrl | string | `nil` | Full Redis cache URL (overrides host/port/user/pass if set) |
| cache.celeryDb | string | `nil` | Redis database number for Celery (default: 0 when unset; legacy redis_celery_db is honored) |
| cache.celeryUrl | string | `nil` | Full Redis Celery URL (overrides host/port/user/pass if set) |
| cache.defaultTimeout | int | `86400` | Default cache timeout in seconds |
| cache.driver | string | `""` | Custom Redis driver (e.g. TLS/managed variants); overrides the redis proto in URLs when set. Ports the legacy supersetNode.connections.redis_driver escape hatch. |
| cache.enabled | bool | `true` | Enable Redis-based features (cache, Celery). Set to false to disable Redis usage entirely. |
| cache.host | string | `nil` | Redis host (default: {{ .Release.Name }}-redis-headless) |
| cache.keyPrefix | string | `"superset_"` | Cache key prefix |
| cache.password | string | `nil` | Redis password |
| cache.port | string | `nil` | Redis port (default: 6379 when unset; legacy supersetNode.connections.redis_port is honored) |
| cache.resultsBackendKeyPrefix | string | `"superset_results"` | Results backend key prefix |
| cache.sentinel | string | `nil` | Redis Sentinel configuration (optional) |
| cache.ssl | object | `{"ca_certs":null,"certfile":null,"enabled":false,"keyfile":null,"ssl_cert_reqs":"required"}` | Redis SSL configuration |
| cache.user | string | `""` | Redis user (optional, for Redis ACL) |
| cluster | object | `{"databaseServiceName":null,"domain":".svc.cluster.local","redisServiceName":null,"websocketServiceName":null}` | Kubernetes cluster configuration Used for constructing service URLs between chart components |
| cluster.databaseServiceName | string | `nil` | Database service name (default: {{ .Release.Name }}-postgresql) Override if using a different service name for the database |
| cluster.domain | string | `".svc.cluster.local"` | Kubernetes cluster domain (default: .svc.cluster.local) Override if using a custom cluster domain |
| cluster.redisServiceName | string | `nil` | Redis service name (default: {{ .Release.Name }}-redis-headless) Override if using a different service name for Redis |
| cluster.websocketServiceName | string | `nil` | WebSocket service name (default: {{ .Release.Name }}-ws) Override if using a different service name for the WebSocket service |
| config | object | `{}` | Superset configuration properties Set any configuration property from superset/config.py here See https://github.com/apache/superset/blob/master/superset/config.py for all available options |
| configFromSecret | string | `"{{ template \"superset.fullname\" . }}-config"` | The name of the secret which we will use to generate a superset_config.py file Note: this secret must have the key superset_config.py in it and can include other files as well |
| configMountPath | string | `"/app/pythonpath"` | |
| configOverrides | object | `{}` | A dictionary of overrides to append at the end of superset_config.py - the name does not matter WARNING: the order is not guaranteed Files can be passed as helm --set-file configOverrides.my-override=my-file.py |
| configOverridesFiles | object | `{}` | Same as above but the values are files |
| database | object | `{"driver":null,"host":null,"name":null,"password":null,"port":null,"ssl":{"enabled":false,"mode":"require"},"uri":null,"user":null}` | Database connection configuration for the Superset metadata database |
| database.driver | string | `nil` | Database driver used when uri is not set (default: postgresql+psycopg2 when unset; legacy supersetNode.connections.db_type is honored when this is unset) |
| database.host | string | `nil` | Database host (default: {{ .Release.Name }}-postgresql) |
| database.name | string | `nil` | Database name (default: superset, resolved via superset.db.name) |
| database.password | string | `nil` | Database password (default: superset, resolved via superset.db.password) ⚠️ CHANGE THIS for production |
| database.port | string | `nil` | Database port (default: 5432 when unset; legacy supersetNode.connections.db_port is honored) |
| database.ssl | object | `{"enabled":false,"mode":"require"}` | Database SSL configuration |
| database.uri | string | `nil` | Full database URI (overrides host/port/user/pass/name if set) Example: "postgresql+psycopg2://user:pass@host:5432/dbname" |
| database.user | string | `nil` | Database user (default: superset, resolved via superset.db.user) |
| envFromSecret | string | `"{{ template \"superset.fullname\" . }}-env"` | The name of the secret which we will use to populate env vars in deployed pods This can be useful for secret keys, etc. |
| envFromSecrets | list | `[]` | This can be a list of templated strings |
| extraConfigMountPath | string | `"/app/configs"` | |
@@ -89,7 +121,9 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| extraSecrets | object | `{}` | Extra files to be mounted as Secrets on the path specified in `configMountPath` |
| extraVolumeMounts | list | `[]` | |
| extraVolumes | list | `[]` | |
| featureFlags | object | `{}` | Feature flags configuration See https://github.com/apache/superset/blob/master/RESOURCES/FEATURE_FLAGS.md |
| 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/ |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"apachesuperset.docker.scarf.sh/apache/superset"` | |
@@ -116,7 +150,7 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| init.enabled | bool | `true` | |
| init.extraContainers | list | `[]` | Launch additional containers into init job pod |
| init.initContainers | list | a container waiting for postgres | List of initContainers |
| init.initscript | string | a script to create admin user and initialize roles | A Superset init script |
| init.initscript | string | unused; kept for backwards-compatibility only | DEPRECATED: this field is no longer used by the chart. The init script is rendered entirely from the internal `superset.initScript` template (which runs `superset db upgrade`, `superset init`, admin creation, and examples). Any customization placed here is silently ignored. See UPGRADING.md. |
| init.jobAnnotations."helm.sh/hook" | string | `"post-install,post-upgrade"` | |
| init.jobAnnotations."helm.sh/hook-delete-policy" | string | `"before-hook-creation"` | |
| init.loadExamples | bool | `false` | |
@@ -143,6 +177,7 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| service.type | string | `"ClusterIP"` | |
| serviceAccount.annotations | object | `{}` | |
| serviceAccount.create | bool | `false` | Create custom service account for Superset. If create: true and serviceAccountName is not provided, `superset.fullname` will be used. |
| serviceAccount.name | string | `""` | Service account name to use (if not specified, defaults to release name + chart name) |
| serviceAccountName | string | `nil` | Specify service account name to be used |
| supersetCeleryBeat.affinity | object | `{}` | Affinity to be added to supersetCeleryBeat deployment |
| supersetCeleryBeat.command | list | a `celery beat` command | Command |
@@ -214,20 +249,7 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetNode.autoscaling.minReplicas | int | `1` | |
| supersetNode.autoscaling.targetCPUUtilizationPercentage | int | `80` | |
| supersetNode.command | list | See `values.yaml` | Startup command |
| supersetNode.connections.db_host | string | `"{{ .Release.Name }}-postgresql"` | |
| supersetNode.connections.db_name | string | `"superset"` | |
| supersetNode.connections.db_pass | string | `"superset"` | |
| supersetNode.connections.db_port | string | `"5432"` | |
| supersetNode.connections.db_type | string | `"postgresql"` | Database type for Superset metadata (Supported types: "postgresql", "mysql") |
| supersetNode.connections.db_user | string | `"superset"` | |
| supersetNode.connections.redis_cache_db | string | `"1"` | |
| supersetNode.connections.redis_celery_db | string | `"0"` | |
| supersetNode.connections.redis_driver | string | `""` | |
| supersetNode.connections.redis_host | string | `"{{ .Release.Name }}-redis-headless"` | Change in case of bringing your own redis and then also set redis.enabled:false |
| supersetNode.connections.redis_port | string | `"6379"` | |
| supersetNode.connections.redis_ssl.enabled | bool | `false` | |
| supersetNode.connections.redis_ssl.ssl_cert_reqs | string | `"CERT_NONE"` | |
| supersetNode.connections.redis_user | string | `""` | |
| supersetNode.connections | object | `{}` | |
| supersetNode.containerSecurityContext | object | `{}` | |
| supersetNode.deploymentAdditionalPodSpec | object | `{}` | Custom pod spec to be added to supersetNode deployment |
| supersetNode.deploymentAnnotations | object | `{}` | Annotations to be added to supersetNode deployment |
@@ -334,6 +356,11 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetWorker.deploymentLabels | object | `{}` | Labels to be added to supersetWorker deployment |
| supersetWorker.extraContainers | list | `[]` | Launch additional containers into supersetWorker pod |
| supersetWorker.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
| supersetWorker.healthCheck | object | `{"enabled":false,"livenessFile":"/tmp/celery_worker_alive","livenessHeartbeatInterval":10,"readinessFile":"/tmp/celery_worker_ready"}` | Celery worker file-based health check (worker writes readiness/liveness files via signals; point supersetWorker.readinessProbe/livenessProbe at these files to use them) |
| supersetWorker.healthCheck.enabled | bool | `false` | Enable the file-based Celery worker health check |
| supersetWorker.healthCheck.livenessFile | string | `"/tmp/celery_worker_alive"` | Liveness file (touched periodically by a heartbeat thread) |
| supersetWorker.healthCheck.livenessHeartbeatInterval | int | `10` | Seconds between liveness heartbeats |
| supersetWorker.healthCheck.readinessFile | string | `"/tmp/celery_worker_ready"` | Readiness file (created when the worker is ready, removed on shutdown) |
| supersetWorker.initContainers | list | a container waiting for postgres and redis | Init container |
| supersetWorker.lifecycle | object | `{}` | Container lifecycle hooks for the worker pod |
| supersetWorker.livenessProbe.exec.command | list | a `celery inspect ping` command | Liveness probe command |

161
helm/superset/UPGRADING.md Normal file
View File

@@ -0,0 +1,161 @@
<!--
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.
-->
# Upgrading the Superset Helm Chart
## Upgrading to chart 0.20.0
Chart 0.20.0 introduces a structured connection schema. The old keys listed below are **DEPRECATED** — they still work (auto-mapped to the new keys at render time) and a deprecation warning is printed in `helm install`/`helm upgrade` NOTES, but they will be removed in a future release. Migrate as soon as possible.
### 1. Connection schema: `supersetNode.connections.*` → `database.*` / `cache.*`
**Before (deprecated — still honored):**
```yaml
supersetNode:
connections:
db_host: "pg-host"
db_port: "5432"
db_user: "superset"
db_pass: "superset"
db_name: "superset"
redis_host: "redis-host"
redis_port: "6379"
redis_cache_db: "1"
redis_celery_db: "0"
redis_driver: "rediss"
```
**After (new structured keys — recommended):**
```yaml
database:
host: "pg-host"
port: 5432
user: "superset"
password: "superset"
name: "superset"
cache:
host: "redis-host"
port: 6379
cacheDb: 1
celeryDb: 0
driver: "rediss" # optional: custom Redis driver / TLS variant
```
Key-by-key mapping summary:
| Old key | New key |
|---|---|
| `supersetNode.connections.db_host` | `database.host` |
| `supersetNode.connections.db_port` | `database.port` |
| `supersetNode.connections.db_user` | `database.user` |
| `supersetNode.connections.db_pass` | `database.password` |
| `supersetNode.connections.db_name` | `database.name` |
| `supersetNode.connections.redis_host` | `cache.host` |
| `supersetNode.connections.redis_port` | `cache.port` |
| `supersetNode.connections.redis_cache_db` | `cache.cacheDb` |
| `supersetNode.connections.redis_celery_db` | `cache.celeryDb` |
| `supersetNode.connections.redis_driver` | `cache.driver` |
### 2. Service account: root `serviceAccountName` → `serviceAccount.name`
**Before (deprecated — still honored):**
```yaml
serviceAccountName: my-sa
```
**After (new key — recommended):**
```yaml
serviceAccount:
name: my-sa
```
### 3. Init script: `init.initscript` is deprecated and replaced by the built-in template
> **This is a behavior change.** The chart no longer uses `init.initscript`. The init script is rendered
> entirely from an internal chart template (`superset.initScript`), which runs the full initialization
> sequence:
>
> 1. `superset db upgrade` — applies all pending database schema migrations
> 2. `superset init` — initializes roles and permissions
> 3. Admin user creation (when `init.createAdmin: true`)
> 4. Example data loading (when `init.loadExamples: true`)
> 5. Datasource import (when `import_datasources.yaml` is present)
>
> A future PR will optionally split the database migration step into a dedicated upgrade Job for
> zero-downtime deployments. Until that PR lands, migrations run as part of the init Job above.
If you customized `init.initscript` in your `values.yaml`, that customization is silently ignored.
Move any customizations to `config` or `configOverrides`:
```yaml
# Move custom Python config here:
config:
MY_SETTING: "value"
configOverrides:
my_custom_override: |
# Python snippet appended to superset_config.py
MY_SETTING = "value"
```
If your use case cannot be covered by `config` or `configOverrides`, please open an issue so the maintainers
can evaluate extending the template.
### 4. New top-level sections: `config.*` and `featureFlags.*`
Two new sections provide direct Superset configuration passthrough without needing raw `configOverrides`.
**`config.*` — direct Superset config properties:**
```yaml
config:
SECRET_KEY: "$(SUPERSET_SECRET_KEY)"
ROW_LIMIT: 50000
WTF_CSRF_ENABLED: true
SQLALCHEMY_POOL_SIZE: 10
```
Each key is injected verbatim into `superset_config.py`. String values are quoted; non-string values
(integers, booleans) are rendered as-is.
**`featureFlags.*` — feature flag overrides:**
```yaml
featureFlags:
ALERT_REPORTS: true
DASHBOARD_RBAC: true
ENABLE_TEMPLATE_PROCESSING: false
```
This is equivalent to setting `FEATURE_FLAGS = {...}` in `superset_config.py`, but is more readable and
schema-validated.
### 5. `values.schema.json` — early validation
The chart now ships a `values.schema.json` that validates the values you provide. Wrong types (e.g., a string
where an integer is expected) or unknown keys in structured sections will cause `helm install`/`helm upgrade`
to fail immediately with a descriptive error instead of producing a broken deployment.
If you get a validation error after upgrading, check that your overridden values match the types documented in
`values.yaml` and the schema.

View File

@@ -35,3 +35,9 @@
echo "Visit http://127.0.0.1:8088 to use your application"
kubectl port-forward service/superset 8088:8088 --namespace {{ .Release.Namespace }}
{{- end }}
{{- $warnings := include "superset.deprecationWarnings" . }}
{{- if trim $warnings }}
⚠️ DEPRECATION WARNINGS
{{ $warnings }}
{{- end }}

View File

@@ -47,11 +47,12 @@ If release name contains chart name it will be used as a full name.
Create the name of the service account to use
*/}}
{{- define "superset.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "superset.fullname" .) .Values.serviceAccountName -}}
{{- else -}}
{{- default "default" .Values.serviceAccountName -}}
{{- end -}}
{{- $name := coalesce .Values.serviceAccount.name .Values.serviceAccountName -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "superset.fullname" .) $name -}}
{{- else -}}
{{- default "default" $name -}}
{{- end -}}
{{- end -}}
{{/*
@@ -105,80 +106,572 @@ app.kubernetes.io/component: {{ .component }}
{{- end -}}
{{- define "superset-config" }}
{{/*
Coalescing resolvers for DB and Redis connection parameters.
Each resolver checks (in order):
1. New top-level database.* / cache.* values
2. Legacy supersetNode.connections.* keys (deprecation path, using safe index to avoid errors on absent maps)
3. cluster.* service name overrides
4. Hard-coded defaults derived from the release name
Call with root context: {{ include "superset.db.host" . }}
*/}}
{{/*
Helper to safely read .Values.supersetNode.connections.<key> without erroring when maps are absent.
*/}}
{{- define "_superset.legacyConn" -}}
{{- $sn := index .Values "supersetNode" | default dict -}}
{{- $conn := index $sn "connections" | default dict -}}
{{- $conn | toJson -}}
{{- end -}}
{{- define "superset.db.host" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- tpl (coalesce .Values.database.host (index $conn "db_host") .Values.cluster.databaseServiceName (printf "%s-postgresql" .Release.Name)) $ -}}
{{- end -}}
{{- define "superset.db.port" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- if .Values.database.port -}}
{{- .Values.database.port | toString -}}
{{- else -}}
{{- coalesce (index $conn "db_port") "5432" -}}
{{- end -}}
{{- end -}}
{{- define "superset.db.user" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- coalesce .Values.database.user (index $conn "db_user") "superset" -}}
{{- end -}}
{{- define "superset.db.password" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- coalesce .Values.database.password (index $conn "db_pass") "superset" -}}
{{- end -}}
{{- define "superset.db.name" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- coalesce .Values.database.name (index $conn "db_name") "superset" -}}
{{- end -}}
{{- define "superset.db.driver" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- coalesce .Values.database.driver (index $conn "db_type") "postgresql+psycopg2" -}}
{{- end -}}
{{- define "superset.redis.host" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- tpl (coalesce .Values.cache.host (index $conn "redis_host") .Values.cluster.redisServiceName (printf "%s-redis-headless" .Release.Name)) $ -}}
{{- end -}}
{{- define "superset.redis.port" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- if .Values.cache.port -}}
{{- .Values.cache.port | toString -}}
{{- else -}}
{{- coalesce (index $conn "redis_port") "6379" -}}
{{- end -}}
{{- end -}}
{{- define "superset.redis.user" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- coalesce .Values.cache.user (index $conn "redis_user") "" -}}
{{- end -}}
{{- define "superset.redis.password" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- coalesce .Values.cache.password (index $conn "redis_password") "" -}}
{{- end -}}
{{- define "superset.redis.cacheDb" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- if not (kindIs "invalid" .Values.cache.cacheDb) -}}
{{- .Values.cache.cacheDb | toString -}}
{{- else -}}
{{- coalesce (index $conn "redis_cache_db") "1" -}}
{{- end -}}
{{- end -}}
{{- define "superset.redis.celeryDb" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- if not (kindIs "invalid" .Values.cache.celeryDb) -}}
{{- .Values.cache.celeryDb | toString -}}
{{- else -}}
{{- coalesce (index $conn "redis_celery_db") "0" -}}
{{- end -}}
{{- end -}}
{{- define "superset.redis.proto" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- if .Values.cache.driver }}{{ .Values.cache.driver }}{{- else if index $conn "redis_driver" }}{{ index $conn "redis_driver" }}{{- else if .Values.cache.ssl.enabled }}rediss{{- else }}redis{{- end -}}
{{- end -}}
{{- define "superset.config" }}
{{- /* SECURITY: Validate admin password is set if admin creation is enabled */}}
{{- if and .Values.init.createAdmin (or (not .Values.init.adminUser.password) (eq .Values.init.adminUser.password "")) }}
{{- fail "SECURITY ERROR: init.createAdmin is true but init.adminUser.password is empty. You must set a secure password using --set init.adminUser.password='your-password' or via external secret." }}
{{- end }}
import os
import json
from urllib.parse import quote
{{- if or .Values.config.cacheConfig .Values.config.dataCacheConfig .Values.config.resultsBackend .Values.config.celeryConfig .Values.cache.enabled }}
from flask_caching.backends.rediscache import RedisCache
{{- end }}
def env(key, default=None):
return os.getenv(key, default)
# Redis Base URL
{{- if .Values.supersetNode.connections.redis_password }}
REDIS_BASE_URL=f"{env('REDIS_DRIVER') or env('REDIS_PROTO')}://{env('REDIS_USER', '')}:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}"
{{- /* Database Configuration - Superset always requires a database */}}
{{- if .Values.database.uri }}
SQLALCHEMY_DATABASE_URI = {{ .Values.database.uri | quote }}
{{- else }}
REDIS_BASE_URL=f"{env('REDIS_DRIVER') or env('REDIS_PROTO')}://{env('REDIS_HOST')}:{env('REDIS_PORT')}"
{{- $driver := include "superset.db.driver" . }}
{{- $sslParams := "" }}
{{- if and (hasKey .Values.database "ssl") .Values.database.ssl.enabled }}
{{- $sslMode := .Values.database.ssl.mode | default "require" }}
{{- $sslParams = printf "?sslmode=%s" $sslMode }}
{{- end }}
SQLALCHEMY_DATABASE_URI = f"{{ $driver }}://{quote(env('DB_USER', ''), safe='')}:{quote(env('DB_PASS', ''), safe='')}@{env('DB_HOST')}:{env('DB_PORT')}/{env('DB_NAME')}{{ $sslParams }}"
{{- end }}
{{- if hasKey .Values.config "SQLALCHEMY_TRACK_MODIFICATIONS" }}
SQLALCHEMY_TRACK_MODIFICATIONS = {{ .Values.config.SQLALCHEMY_TRACK_MODIFICATIONS | toString | title }}
{{- else }}
SQLALCHEMY_TRACK_MODIFICATIONS = False
{{- end }}
# Redis URL Params
{{- if .Values.supersetNode.connections.redis_ssl.enabled }}
REDIS_URL_PARAMS = f"?ssl_cert_reqs={env('REDIS_SSL_CERT_REQS')}"
{{- /* Redis Configuration - only if Redis cache is configured */}}
{{- if .Values.cache.enabled }}
{{- if .Values.cache.cacheUrl }}
CACHE_REDIS_URL = {{ .Values.cache.cacheUrl | quote }}
{{- else }}
{{- $useSSL := and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }}
_redis_user = quote(env('REDIS_USER', ''), safe='')
_redis_password = quote(env('REDIS_PASSWORD', ''), safe='')
_redis_auth = f"{_redis_user}:{_redis_password}@" if (_redis_user or _redis_password) else ""
REDIS_BASE_URL = f"{{ include "superset.redis.proto" . }}://{_redis_auth}{env('REDIS_HOST')}:{env('REDIS_PORT')}"
{{- if $useSSL }}
{{- $sslCertReqs := .Values.cache.ssl.ssl_cert_reqs | default "required" }}
REDIS_URL_PARAMS = f"?ssl_cert_reqs={{ $sslCertReqs }}"
{{- else }}
REDIS_URL_PARAMS = ""
{{- end}}
{{- end }}
{{- $cacheDb := include "superset.redis.cacheDb" . }}
CACHE_REDIS_URL = f"{REDIS_BASE_URL}/{{ $cacheDb }}{REDIS_URL_PARAMS}"
{{- end }}
{{- if .Values.cache.celeryUrl }}
CELERY_REDIS_URL = {{ .Values.cache.celeryUrl | quote }}
{{- else if not .Values.cache.cacheUrl }}
{{- $celeryDb := include "superset.redis.celeryDb" . }}
CELERY_REDIS_URL = f"{REDIS_BASE_URL}/{{ $celeryDb }}{REDIS_URL_PARAMS}"
{{- else }}
{{- if or .Values.config.celeryConfig (not .Values.cache.enabled) }}
{{- /* Custom celeryConfig provided or cache disabled - OK */}}
{{- else }}
{{- fail "CONFIGURATION ERROR: cache.cacheUrl is set but cache.celeryUrl is not set. When using cacheUrl, you must also set celeryUrl for Celery to work. Alternatively, set config.celeryConfig to provide a custom Celery configuration." }}
{{- end }}
{{- end }}
{{- end }}
# Build Redis URLs
CACHE_REDIS_URL = f"{REDIS_BASE_URL}/{env('REDIS_DB', 1)}{REDIS_URL_PARAMS}"
CELERY_REDIS_URL = f"{REDIS_BASE_URL}/{env('REDIS_CELERY_DB', 0)}{REDIS_URL_PARAMS}"
MAPBOX_API_KEY = env('MAPBOX_API_KEY', '')
{{- /* Cache Configuration */}}
{{- if .Values.config.cacheConfig }}
CACHE_CONFIG = json.loads({{ .Values.config.cacheConfig | toJson | quote }})
{{- else if .Values.cache.enabled }}
CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 300,
'CACHE_KEY_PREFIX': 'superset_',
'CACHE_REDIS_URL': CACHE_REDIS_URL,
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': {{ .Values.cache.defaultTimeout | default (.Values.config.cacheDefaultTimeout | default 86400) | int }},
'CACHE_KEY_PREFIX': {{ .Values.cache.keyPrefix | default "superset_" | quote }},
'CACHE_REDIS_URL': CACHE_REDIS_URL,
}
{{- end }}
{{- if .Values.config.dataCacheConfig }}
DATA_CACHE_CONFIG = json.loads({{ .Values.config.dataCacheConfig | toJson | quote }})
{{- else if .Values.config.cacheConfig }}
DATA_CACHE_CONFIG = CACHE_CONFIG
{{- else if .Values.cache.enabled }}
DATA_CACHE_CONFIG = CACHE_CONFIG
{{- end }}
{{- /* SQLLAB_ASYNC_TIME_LIMIT_SEC - Required for async_queries module import (default: 6 hours) */}}
{{- if .Values.config.SQLLAB_ASYNC_TIME_LIMIT_SEC }}
SQLLAB_ASYNC_TIME_LIMIT_SEC = {{ .Values.config.SQLLAB_ASYNC_TIME_LIMIT_SEC | int }}
{{- else }}
from datetime import timedelta
SQLLAB_ASYNC_TIME_LIMIT_SEC = int(timedelta(hours=6).total_seconds())
{{- end }}
if os.getenv("SQLALCHEMY_DATABASE_URI"):
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI")
else:
{{- if eq .Values.supersetNode.connections.db_type "postgresql" }}
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('DB_USER')}:{os.getenv('DB_PASS')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
{{- else if eq .Values.supersetNode.connections.db_type "mysql" }}
SQLALCHEMY_DATABASE_URI = f"mysql+mysqldb://{os.getenv('DB_USER')}:{os.getenv('DB_PASS')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
{{- else }}
{{ fail (printf "Unsupported database type: %s. Please use 'postgresql' or 'mysql'." .Values.supersetNode.connections.db_type) }}
{{- end }}
{{- /* Celery Configuration */}}
{{- if .Values.config.celeryConfig }}
{{- if kindIs "string" .Values.config.celeryConfig }}
{{ .Values.config.celeryConfig }}
{{- else }}
class CeleryConfig:
{{- range $key, $value := .Values.config.celeryConfig }}
{{ $key }} = json.loads({{ $value | toJson | quote }})
{{- end }}
{{- if hasKey .Values.config.celeryConfig "imports" }}
CELERY_IMPORTS = CeleryConfig.imports
{{- else }}
CELERY_IMPORTS = ()
{{- end }}
CELERY_CONFIG = CeleryConfig
{{- end }}
{{- else if .Values.cache.enabled }}
from celery.schedules import crontab
from datetime import timedelta
class CeleryConfig:
imports = ("superset.sql_lab", )
broker_url = CELERY_REDIS_URL
result_backend = CELERY_REDIS_URL
imports = (
"superset.sql_lab",
"superset.tasks.scheduler",
"superset.tasks.thumbnails",
"superset.tasks.cache",
)
broker_connection_retry_on_startup = True
worker_prefetch_multiplier = 10
task_acks_late = True
broker_url = CELERY_REDIS_URL
result_backend = CELERY_REDIS_URL
task_annotations = {
"sql_lab.get_sql_results": {
"rate_limit": "100/s",
},
}
beat_schedule = {
"reports.scheduler": {
"task": "reports.scheduler",
"schedule": crontab(minute="*", hour="*"),
"options": {"expires": int(timedelta(weeks=1).total_seconds())},
},
"reports.prune_log": {
"task": "reports.prune_log",
"schedule": crontab(minute=0, hour=0),
},
}
CELERY_IMPORTS = CeleryConfig.imports
CELERY_CONFIG = CeleryConfig
RESULTS_BACKEND = RedisCache(
host=env('REDIS_HOST'),
{{- if .Values.supersetNode.connections.redis_password }}
password=env('REDIS_PASSWORD'),
{{- end }}
port=env('REDIS_PORT'),
key_prefix='superset_results',
{{- if .Values.supersetNode.connections.redis_ssl.enabled }}
ssl=True,
ssl_cert_reqs=env('REDIS_SSL_CERT_REQS'),
{{- end }}
)
{{- end }}
{{ if .Values.configOverrides }}
# Overrides
{{- /* Celery Worker Health Check - File-based health probes for Kubernetes */}}
{{- if and .Values.supersetWorker.healthCheck .Values.supersetWorker.healthCheck.enabled }}
# Celery Worker Health Check Configuration
import threading
from celery import bootsteps
from celery.signals import worker_ready, worker_shutdown, worker_init
_readiness_file = {{ .Values.supersetWorker.healthCheck.readinessFile | default "/tmp/celery_worker_ready" | quote }}
_liveness_file = {{ .Values.supersetWorker.healthCheck.livenessFile | default "/tmp/celery_worker_alive" | quote }}
_heartbeat_interval = {{ .Values.supersetWorker.healthCheck.livenessHeartbeatInterval | default 10 | int }}
_liveness_thread = None
_liveness_stop_event = None
@worker_ready.connect
def create_ready_file(sender, **kwargs):
try:
open(_readiness_file, 'w').close()
except Exception as e:
print(f"Warning: Could not create readiness file: {e}")
@worker_shutdown.connect
def remove_ready_file(sender, **kwargs):
global _liveness_thread, _liveness_stop_event
if _liveness_stop_event:
_liveness_stop_event.set()
if _liveness_thread:
_liveness_thread.join(timeout=5)
try:
if os.path.exists(_readiness_file):
os.remove(_readiness_file)
if os.path.exists(_liveness_file):
os.remove(_liveness_file)
except Exception as e:
print(f"Warning: Could not remove health check files: {e}")
@worker_init.connect
def start_liveness_heartbeat(sender, **kwargs):
global _liveness_thread, _liveness_stop_event
_liveness_stop_event = threading.Event()
def update_liveness():
while not _liveness_stop_event.is_set():
try:
with open(_liveness_file, 'w') as f:
f.write(str(os.getpid()))
except Exception as e:
print(f"Warning: Could not update liveness file: {e}")
_liveness_stop_event.wait(_heartbeat_interval)
_liveness_thread = threading.Thread(target=update_liveness, daemon=True)
_liveness_thread.start()
{{- else }}
CELERY_WORKER_HEALTH_CHECK_ENABLED = False
{{- end }}
{{- /* Results Backend */}}
{{- $redisHostForBackend := include "superset.redis.host" . }}
{{- $redisPortForBackend := include "superset.redis.port" . }}
{{- $redisPasswordForBackend := include "superset.redis.password" . }}
{{- if .Values.config.resultsBackend }}
{{- if kindIs "string" .Values.config.resultsBackend }}
RESULTS_BACKEND = {{ .Values.config.resultsBackend }}
{{- else }}
RESULTS_BACKEND = RedisCache(
host={{ $redisHostForBackend | quote }},
{{- if $redisPasswordForBackend }}
password={{ $redisPasswordForBackend | quote }},
{{- end }}
port={{ $redisPortForBackend | int }},
key_prefix={{ .Values.cache.resultsBackendKeyPrefix | default "superset_results" | quote }},
{{- if and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }}
ssl=True,
ssl_cert_reqs={{ .Values.cache.ssl.ssl_cert_reqs | default "required" | quote }},
{{- end }}
)
{{- end }}
{{- else if .Values.cache.enabled }}
RESULTS_BACKEND = RedisCache(
host={{ $redisHostForBackend | quote }},
{{- if $redisPasswordForBackend }}
password={{ $redisPasswordForBackend | quote }},
{{- end }}
port={{ $redisPortForBackend | int }},
key_prefix={{ .Values.cache.resultsBackendKeyPrefix | default "superset_results" | quote }},
{{- if and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }}
ssl=True,
ssl_cert_reqs={{ .Values.cache.ssl.ssl_cert_reqs | default "required" | quote }},
{{- end }}
)
{{- end }}
{{- /* Global Async Queries Cache Backend */}}
{{- $redisUserForGaq := include "superset.redis.user" . }}
{{- if .Values.config.GLOBAL_ASYNC_QUERIES_CACHE_BACKEND }}
GLOBAL_ASYNC_QUERIES_CACHE_BACKEND = json.loads({{ .Values.config.GLOBAL_ASYNC_QUERIES_CACHE_BACKEND | toJson | quote }})
{{- else if .Values.cache.enabled }}
GLOBAL_ASYNC_QUERIES_CACHE_BACKEND = {
"CACHE_TYPE": "RedisCache",
"CACHE_REDIS_HOST": {{ $redisHostForBackend | quote }},
"CACHE_REDIS_PORT": {{ $redisPortForBackend | int }},
{{- if $redisUserForGaq }}
"CACHE_REDIS_USER": {{ $redisUserForGaq | quote }},
{{- end }}
{{- if $redisPasswordForBackend }}
"CACHE_REDIS_PASSWORD": {{ $redisPasswordForBackend | quote }},
{{- else }}
"CACHE_REDIS_PASSWORD": "",
{{- end }}
"CACHE_REDIS_DB": {{ .Values.cache.asyncQueries.db | default .Values.cache.cacheDb | default 0 | int }},
"CACHE_KEY_PREFIX": {{ .Values.cache.asyncQueries.keyPrefix | default "qc-" | quote }},
"CACHE_DEFAULT_TIMEOUT": {{ .Values.cache.asyncQueries.timeout | default 86400 | int }},
{{- if and .Values.cache.sentinel .Values.cache.sentinel.enabled }}
{{- if .Values.cache.sentinel.sentinels }}
"CACHE_REDIS_SENTINELS": {{ .Values.cache.sentinel.sentinels | toJson }},
{{- else }}
{{- fail "CONFIGURATION ERROR: cache.sentinel.enabled is true but cache.sentinel.sentinels is not set. You must provide Sentinel host(s) in cache.sentinel.sentinels (e.g., [['sentinel-host', 26379]])." }}
{{- end }}
"CACHE_REDIS_SENTINEL_MASTER": {{ .Values.cache.sentinel.master | default "mymaster" | quote }},
{{- if .Values.cache.sentinel.password }}
"CACHE_REDIS_SENTINEL_PASSWORD": {{ .Values.cache.sentinel.password | quote }},
{{- else }}
"CACHE_REDIS_SENTINEL_PASSWORD": None,
{{- end }}
{{- end }}
{{- if and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }}
"CACHE_REDIS_SSL": True,
"CACHE_REDIS_SSL_CERTFILE": {{ if .Values.cache.ssl.certfile }}{{ .Values.cache.ssl.certfile | quote }}{{ else }}None{{ end }},
"CACHE_REDIS_SSL_KEYFILE": {{ if .Values.cache.ssl.keyfile }}{{ .Values.cache.ssl.keyfile | quote }}{{ else }}None{{ end }},
"CACHE_REDIS_SSL_CERT_REQS": {{ .Values.cache.ssl.ssl_cert_reqs | default "required" | quote }},
"CACHE_REDIS_SSL_CA_CERTS": {{ if .Values.cache.ssl.ca_certs }}{{ .Values.cache.ssl.ca_certs | quote }}{{ else }}None{{ end }},
{{- else }}
"CACHE_REDIS_SSL": False,
"CACHE_REDIS_SSL_CERTFILE": None,
"CACHE_REDIS_SSL_KEYFILE": None,
"CACHE_REDIS_SSL_CERT_REQS": {{ .Values.cache.ssl.ssl_cert_reqs | default "required" | quote }},
"CACHE_REDIS_SSL_CA_CERTS": None,
{{- end }}
}
{{- end }}
{{- /* Global Async Queries Results Backend */}}
{{- if .Values.config.GLOBAL_ASYNC_QUERIES_RESULTS_BACKEND }}
GLOBAL_ASYNC_QUERIES_RESULTS_BACKEND = json.loads({{ .Values.config.GLOBAL_ASYNC_QUERIES_RESULTS_BACKEND | toJson | quote }})
{{- else if .Values.cache.enabled }}
GLOBAL_ASYNC_QUERIES_RESULTS_BACKEND = {
"backend": "redis",
"host": {{ $redisHostForBackend | quote }},
"port": {{ $redisPortForBackend | int }},
"prefix": {{ .Values.cache.asyncQueries.keyPrefix | default "qc-" | quote }},
"db": {{ .Values.cache.asyncQueries.db | default .Values.cache.cacheDb | default 0 | int }},
{{- if $redisPasswordForBackend }}
"password": {{ $redisPasswordForBackend | quote }},
{{- end }}
}
{{- end }}
{{- /* Feature Flags */}}
{{- if .Values.featureFlags }}
FEATURE_FLAGS = {
{{- range $key, $value := .Values.featureFlags }}
{{- if kindIs "bool" $value }}
"{{ $key }}": {{ if $value }}True{{ else }}False{{ end }},
{{- else if kindIs "string" $value }}
"{{ $key }}": {{ $value | quote }},
{{- else if kindIs "float64" $value }}
"{{ $key }}": {{ $value }},
{{- else if kindIs "int" $value }}
"{{ $key }}": {{ $value }},
{{- else if kindIs "invalid" $value }}
"{{ $key }}": None,
{{- else }}
"{{ $key }}": json.loads({{ $value | toJson | quote }}),
{{- end }}
{{- end }}
}
{{- end }}
{{- /* FAB Security API - Required for List Roles view in 6.0.0+ */}}
{{- if not (hasKey .Values.config "FAB_ADD_SECURITY_API") }}
FAB_ADD_SECURITY_API = True
{{- end }}
{{- if not (hasKey .Values.config "FAB_ADD_SECURITY_VIEWS") }}
FAB_ADD_SECURITY_VIEWS = True
{{- end }}
{{- /* Global Async Queries Transport - Auto-configure for websockets if enabled */}}
{{- if .Values.config.GLOBAL_ASYNC_QUERIES_TRANSPORT }}
GLOBAL_ASYNC_QUERIES_TRANSPORT = {{ .Values.config.GLOBAL_ASYNC_QUERIES_TRANSPORT | quote }}
{{- else if .Values.supersetWebsockets.enabled }}
GLOBAL_ASYNC_QUERIES_TRANSPORT = "ws"
{{- else }}
GLOBAL_ASYNC_QUERIES_TRANSPORT = "polling"
{{- end }}
{{- /* Global Async Queries WebSocket URL */}}
{{- $wsUrl := "" }}
{{- if .Values.config.GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL }}
{{- $wsUrl = .Values.config.GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL }}
GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL = {{ $wsUrl | quote }}
{{- else if and .Values.supersetWebsockets.enabled .Values.supersetWebsockets.websocketUrl }}
{{- $wsUrl = .Values.supersetWebsockets.websocketUrl }}
GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL = {{ $wsUrl | quote }}
{{- else if .Values.supersetWebsockets.enabled }}
{{- $wsServiceName := .Values.cluster.websocketServiceName }}
{{- if not $wsServiceName }}
{{- $wsServiceName = printf "%s-ws" (include "superset.fullname" .) }}
{{- end }}
{{- $wsPort := .Values.supersetWebsockets.service.port | default 8080 }}
{{- $wsPath := "/ws" }}
{{- $clusterDomain := .Values.cluster.domain | default ".svc.cluster.local" }}
{{- $wsUrl = printf "ws://%s.%s%s:%d%s" $wsServiceName .Release.Namespace $clusterDomain $wsPort $wsPath }}
GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL = {{ $wsUrl | quote }}
{{- end }}
{{- /* Global Async Queries JWT Secret */}}
{{- if .Values.config.GLOBAL_ASYNC_QUERIES_JWT_SECRET }}
GLOBAL_ASYNC_QUERIES_JWT_SECRET = {{ .Values.config.GLOBAL_ASYNC_QUERIES_JWT_SECRET | quote }}
{{- else if and .Values.supersetWebsockets.enabled .Values.supersetWebsockets.config.jwtSecret }}
GLOBAL_ASYNC_QUERIES_JWT_SECRET = {{ .Values.supersetWebsockets.config.jwtSecret | quote }}
{{- end }}
{{- /* Global Async Queries JWT Cookie Settings */}}
{{- if hasKey .Values.config "GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE" }}
GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = {{ .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE | toString | title }}
{{- else if and .Values.supersetWebsockets.enabled (or (hasPrefix "wss://" $wsUrl) .Values.ingress.tls) }}
GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = True
{{- else if .Values.supersetWebsockets.enabled }}
GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = False
{{- end }}
{{- if .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE }}
GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE = {{ .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE | quote }}
{{- else if .Values.supersetWebsockets.enabled }}
GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE = "Lax"
{{- end }}
{{- if .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME }}
GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME = {{ .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME | quote }}
{{- else if and .Values.supersetWebsockets.enabled .Values.supersetWebsockets.config.jwtCookieName }}
GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME = {{ .Values.supersetWebsockets.config.jwtCookieName | quote }}
{{- end }}
{{- /* Content Security Policy (CSP) */}}
{{- if and .Values.supersetWebsockets.enabled $wsUrl (not (hasKey .Values.config "TALISMAN_CONFIG")) }}
TALISMAN_CONFIG = {
"content_security_policy": {
"base-uri": ["'self'"],
"default-src": ["'self'"],
"img-src": [
"'self'",
"blob:",
"data:",
"https://apachesuperset.gateway.scarf.sh",
"https://static.scarf.sh/",
"ows.terrestris.de",
"https://cdn.document360.io",
],
"worker-src": ["'self'", "blob:"],
"connect-src": [
"'self'",
{{ $wsUrl | quote }},
"https://api.mapbox.com",
"https://events.mapbox.com",
"https://tile.openstreetmap.org",
"https://tile.osm.ch",
],
"object-src": "'none'",
"style-src": [
"'self'",
"'unsafe-inline'",
],
"script-src": ["'self'", "'strict-dynamic'"],
},
"content_security_policy_nonce_in": ["script-src"],
{{- if or (hasPrefix "wss://" $wsUrl) .Values.ingress.tls }}
"force_https": True,
"session_cookie_secure": True,
{{- else }}
"force_https": False,
"session_cookie_secure": False,
{{- end }}
}
{{- end }}
{{- /* General Configuration - iterate through all config values */}}
{{- range $key, $value := .Values.config }}
{{- if and (ne $key "cacheConfig") (ne $key "dataCacheConfig") (ne $key "celeryConfig") (ne $key "resultsBackend") (ne $key "GLOBAL_ASYNC_QUERIES_CACHE_BACKEND") (ne $key "GLOBAL_ASYNC_QUERIES_RESULTS_BACKEND") (ne $key "GLOBAL_ASYNC_QUERIES_TRANSPORT") (ne $key "GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL") (ne $key "GLOBAL_ASYNC_QUERIES_JWT_SECRET") (ne $key "GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE") (ne $key "GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE") (ne $key "GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME") (ne $key "TALISMAN_CONFIG") (ne $key "SQLLAB_ASYNC_TIME_LIMIT_SEC") (ne $key "SQLALCHEMY_TRACK_MODIFICATIONS") }}
{{- if kindIs "map" $value }}
{{ $key }} = json.loads({{ $value | toJson | quote }})
{{- else if kindIs "slice" $value }}
{{ $key }} = json.loads({{ $value | toJson | quote }})
{{- else if kindIs "bool" $value }}
{{ $key }} = {{ if $value }}True{{ else }}False{{ end }}
{{- else if kindIs "string" $value }}
{{- if or (hasPrefix "f\"" $value) (hasPrefix "F\"" $value) (hasPrefix "r\"" $value) (hasPrefix "R\"" $value) (hasPrefix "b\"" $value) (hasPrefix "B\"" $value) }}
{{ $key }} = {{ $value }}
{{- else }}
{{ $key }} = {{ $value | quote }}
{{- end }}
{{- else if kindIs "invalid" $value }}
{{ $key }} = None
{{- else }}
{{ $key }} = {{ $value | toJson }}
{{- end }}
{{- end }}
{{- end }}
{{- /* Custom Config Overrides */}}
{{- if .Values.configOverrides }}
# Custom Overrides
{{- range $key, $value := .Values.configOverrides }}
# {{ $key }}
{{ tpl $value $ }}
{{- end }}
{{- end }}
{{ if .Values.configOverridesFiles }}
{{- if .Values.configOverridesFiles }}
# Overrides from files
{{- $files := .Files }}
{{- range $key, $value := .Values.configOverridesFiles }}
@@ -187,7 +680,57 @@ RESULTS_BACKEND = RedisCache(
{{- end }}
{{- end }}
{{- end -}}
{{- define "superset.initScript" -}}
#!/bin/sh
set -eu
echo "Upgrading DB schema..."
superset db upgrade
echo "Initializing roles and permissions..."
superset init
echo "Init job: Creating admin user and loading initial data..."
{{- if .Values.init.createAdmin }}
echo "Creating admin user (if not present)..."
if superset fab list-users 2>/dev/null | grep -qF {{ printf "username:%s" .Values.init.adminUser.username | squote }}; then
echo "Admin user already exists, skipping."
else
superset fab create-admin \
--username {{ .Values.init.adminUser.username | squote }} \
--firstname {{ .Values.init.adminUser.firstname | squote }} \
--lastname {{ .Values.init.adminUser.lastname | squote }} \
--email {{ .Values.init.adminUser.email | squote }} \
--password {{ .Values.init.adminUser.password | squote }}
fi
{{- else }}
echo "Skipping admin creation (init.createAdmin=false)"
{{- end }}
{{- if .Values.init.loadExamples }}
echo "Loading examples..."
superset load_examples
{{- else }}
echo "Skipping examples (init.loadExamples=false)"
{{- end }}
if [ -f "{{ .Values.extraConfigMountPath }}/import_datasources.yaml" ]; then
echo "Importing database connections..."
superset import_datasources -p {{ .Values.extraConfigMountPath }}/import_datasources.yaml
fi
echo "Init job complete."
{{- end -}}
{{/*
Deprecation warnings returns a newline-separated list of active deprecation messages,
or empty string when no deprecated keys are set. Rendered in NOTES.txt after install/upgrade.
*/}}
{{- define "superset.deprecationWarnings" -}}
{{- $conn := include "_superset.legacyConn" . | fromJson -}}
{{- if gt (len (keys $conn)) 0 }}
- supersetNode.connections.* is deprecated; use database.* and cache.* (auto-mapped for now). See UPGRADING.md
{{- end }}
{{- if .Values.serviceAccountName }}
- root serviceAccountName is deprecated; use serviceAccount.name (auto-mapped for now). See UPGRADING.md
{{- end }}
{{- end -}}
{{- define "supersetNode.selectorLabels" -}}
app.kubernetes.io/name: {{ include "superset.name" . }}

View File

@@ -37,9 +37,12 @@ spec:
template:
metadata:
annotations:
checksum/superset_config.py: {{ include "superset-config" . | sha256sum }}
checksum/superset_config.py: {{ include "superset.config" . | sha256sum }}
checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }}
checksum/connections: {{ .Values.supersetNode.connections | toYaml | 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 }}
@@ -52,6 +55,9 @@ spec:
{{- if .Values.supersetCeleryBeat.podAnnotations }}
{{- toYaml .Values.supersetCeleryBeat.podAnnotations | nindent 8 }}
{{- end }}
{{- with .Values.globalPodAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "supersetCeleryBeat.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetCeleryBeat.podLabels }}

View File

@@ -36,11 +36,14 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include "superset-config" . | sha256sum }}
checksum/config: {{ include "superset.config" . | sha256sum }}
checksum/secrets: {{ tpl (toJson .Values.extraSecretEnv) . | sha256sum }}
{{- if .Values.supersetCeleryFlower.podAnnotations }}
{{- toYaml .Values.supersetCeleryFlower.podAnnotations | nindent 8 }}
{{- end }}
{{- with .Values.globalPodAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "supersetCeleryFlower.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetCeleryFlower.podLabels }}

View File

@@ -43,9 +43,12 @@ spec:
template:
metadata:
annotations:
checksum/superset_config.py: {{ include "superset-config" . | sha256sum }}
checksum/superset_config.py: {{ include "superset.config" . | sha256sum }}
checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }}
checksum/connections: {{ .Values.supersetNode.connections | toYaml | 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 }}
@@ -58,6 +61,9 @@ spec:
{{- if .Values.supersetWorker.podAnnotations }}
{{- toYaml .Values.supersetWorker.podAnnotations | nindent 8 }}
{{- end }}
{{- with .Values.globalPodAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "supersetWorker.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetWorker.podLabels }}

View File

@@ -44,6 +44,9 @@ spec:
{{- if .Values.supersetWebsockets.podAnnotations }}
{{- toYaml .Values.supersetWebsockets.podAnnotations | nindent 8 }}
{{- end }}
{{- with .Values.globalPodAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "supersetWebsockets.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetWebsockets.podLabels }}

View File

@@ -44,10 +44,13 @@ spec:
metadata:
annotations:
# Force reload on config changes
checksum/superset_config.py: {{ include "superset-config" . | sha256sum }}
checksum/superset_init.sh: {{ tpl .Values.init.initscript . | sha256sum }}
checksum/superset_config.py: {{ include "superset.config" . | sha256sum }}
checksum/superset_init.sh: {{ include "superset.initScript" . | sha256sum }}
checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }}
checksum/connections: {{ .Values.supersetNode.connections | toYaml | 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 }}
@@ -60,6 +63,9 @@ spec:
{{- if .Values.supersetNode.podAnnotations }}
{{- toYaml .Values.supersetNode.podAnnotations | nindent 8 }}
{{- end }}
{{- with .Values.globalPodAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "supersetNode.selectorLabels" . | nindent 8 }}
{{- if .Values.supersetNode.podLabels }}

View File

@@ -26,24 +26,28 @@ metadata:
{{- include "superset.labels" . | nindent 4 }}
type: Opaque
stringData:
REDIS_HOST: {{ tpl .Values.supersetNode.connections.redis_host . | quote }}
REDIS_USER: {{ .Values.supersetNode.connections.redis_user | quote }}
{{- if .Values.supersetNode.connections.redis_password }}
REDIS_PASSWORD: {{ .Values.supersetNode.connections.redis_password | quote }}
DB_HOST: {{ include "superset.db.host" . | quote }}
DB_PORT: {{ include "superset.db.port" . | quote }}
DB_USER: {{ include "superset.db.user" . | quote }}
DB_PASS: {{ include "superset.db.password" . | quote }}
DB_NAME: {{ include "superset.db.name" . | quote }}
{{- if .Values.database.ssl.enabled }}
DB_SSL_MODE: {{ .Values.database.ssl.mode | default "require" | quote }}
{{- end }}
{{- if .Values.cache.enabled }}
REDIS_HOST: {{ include "superset.redis.host" . | quote }}
REDIS_PORT: {{ include "superset.redis.port" . | quote }}
REDIS_USER: {{ include "superset.redis.user" . | quote }}
{{- if include "superset.redis.password" . }}
REDIS_PASSWORD: {{ include "superset.redis.password" . | quote }}
{{- end }}
REDIS_DB: {{ include "superset.redis.cacheDb" . | quote }}
REDIS_CELERY_DB: {{ include "superset.redis.celeryDb" . | quote }}
REDIS_PROTO: {{ include "superset.redis.proto" . | quote }}
{{- if .Values.cache.driver }}
REDIS_DRIVER: {{ .Values.cache.driver | quote }}
{{- end }}
REDIS_PORT: {{ .Values.supersetNode.connections.redis_port | quote }}
REDIS_PROTO: {{ if .Values.supersetNode.connections.redis_ssl.enabled }}"rediss"{{ else }}"redis"{{ end }}
REDIS_DRIVER: {{ .Values.supersetNode.connections.redis_driver | quote }}
REDIS_DB: {{ .Values.supersetNode.connections.redis_cache_db | quote }}
REDIS_CELERY_DB: {{ .Values.supersetNode.connections.redis_celery_db | quote }}
{{- if .Values.supersetNode.connections.redis_ssl.enabled }}
REDIS_SSL_CERT_REQS: {{ .Values.supersetNode.connections.redis_ssl.ssl_cert_reqs | default "CERT_NONE" | quote }}
{{- end }}
DB_HOST: {{ tpl .Values.supersetNode.connections.db_host . | quote }}
DB_PORT: {{ .Values.supersetNode.connections.db_port | quote }}
DB_USER: {{ .Values.supersetNode.connections.db_user | quote }}
DB_PASS: {{ .Values.supersetNode.connections.db_pass | quote }}
DB_NAME: {{ .Values.supersetNode.connections.db_name | quote }}
{{- if .Values.extraSecretEnv }}
{{- range $key, $value := .Values.extraSecretEnv }}
{{ $key }}: {{ $value | quote }}

View File

@@ -27,9 +27,9 @@ metadata:
type: Opaque
stringData:
superset_config.py: |
{{- include "superset-config" . | nindent 4 }}
{{- include "superset.config" . | nindent 4 }}
superset_init.sh: |
{{- tpl .Values.init.initscript . | nindent 4 }}
{{- include "superset.initScript" . | nindent 4 }}
superset_bootstrap.sh: |
{{- tpl .Values.bootstrapScript . | nindent 4 }}

View File

@@ -0,0 +1,132 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
suite: config checksums + global pod annotations
templates:
- deployment.yaml
- deployment-worker.yaml
- deployment-beat.yaml
- deployment-flower.yaml
- deployment-ws.yaml
tests:
- it: main deployment has new config checksums, not the old connections checksum
template: deployment.yaml
asserts:
- exists:
path: 'spec.template.metadata.annotations["checksum/database"]'
- exists:
path: 'spec.template.metadata.annotations["checksum/config"]'
- notExists:
path: 'spec.template.metadata.annotations["checksum/connections"]'
- it: main deployment applies globalPodAnnotations
template: deployment.yaml
set:
globalPodAnnotations:
team: data
asserts:
- equal:
path: 'spec.template.metadata.annotations.team'
value: data
- it: globalPodAnnotations coexist with per-component podAnnotations on deployment.yaml
template: deployment.yaml
set:
globalPodAnnotations:
global: "yes"
supersetNode:
podAnnotations:
component: "yes"
asserts:
- equal:
path: 'spec.template.metadata.annotations.global'
value: "yes"
- equal:
path: 'spec.template.metadata.annotations.component'
value: "yes"
- it: worker deployment has config and database checksums
template: deployment-worker.yaml
asserts:
- exists:
path: 'spec.template.metadata.annotations["checksum/database"]'
- exists:
path: 'spec.template.metadata.annotations["checksum/config"]'
- it: worker deployment applies globalPodAnnotations
template: deployment-worker.yaml
set:
globalPodAnnotations:
team: data
asserts:
- equal:
path: 'spec.template.metadata.annotations.team'
value: data
- it: beat deployment has config and database checksums
template: deployment-beat.yaml
set:
supersetCeleryBeat:
enabled: true
asserts:
- exists:
path: 'spec.template.metadata.annotations["checksum/database"]'
- exists:
path: 'spec.template.metadata.annotations["checksum/config"]'
- it: beat deployment applies globalPodAnnotations
template: deployment-beat.yaml
set:
supersetCeleryBeat:
enabled: true
globalPodAnnotations:
team: data
asserts:
- equal:
path: 'spec.template.metadata.annotations.team'
value: data
- it: flower deployment has config checksum
template: deployment-flower.yaml
set:
supersetCeleryFlower:
enabled: true
asserts:
- exists:
path: 'spec.template.metadata.annotations["checksum/config"]'
- it: flower deployment applies globalPodAnnotations
template: deployment-flower.yaml
set:
supersetCeleryFlower:
enabled: true
globalPodAnnotations:
team: data
asserts:
- equal:
path: 'spec.template.metadata.annotations.team'
value: data
- it: websockets deployment has wsconfig checksum
template: deployment-ws.yaml
set:
supersetWebsockets:
enabled: true
asserts:
- exists:
path: 'spec.template.metadata.annotations["checksum/wsconfig"]'
- it: websockets deployment applies globalPodAnnotations
template: deployment-ws.yaml
set:
supersetWebsockets:
enabled: true
globalPodAnnotations:
team: data
asserts:
- equal:
path: 'spec.template.metadata.annotations.team'
value: data

View File

@@ -0,0 +1,94 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
suite: superset_config.py rendering
templates:
- secret-superset-config.yaml
tests:
- it: builds SQLALCHEMY_DATABASE_URI from env with percent-encoded creds
set: {database: {host: pg.svc, port: 5432, user: su, password: pw, name: sset}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'SQLALCHEMY_DATABASE_URI = f"postgresql\+psycopg2://\{quote\(env\(.DB_USER'}
- it: honors database.uri override
set: {database.uri: "postgresql://x:y@h:5432/d"}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'SQLALCHEMY_DATABASE_URI = "postgresql://x:y@h:5432/d"'}
- it: imports required for env/json handling are present
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'import json'}
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'from urllib\.parse import quote'}
- it: redis creds are percent-encoded and read from env
set: {cache: {enabled: true, password: "p@ss"}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'quote\(env\(.REDIS_PASSWORD'}
- it: config map values render via json.loads
set: {config: {MY_MAP: {enabled: true}}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'MY_MAP = json\.loads\('}
- it: config nil values render as None
set: {config: {MY_NIL: null}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'MY_NIL = None'}
- it: supersetWorker healthCheck enabled renders readiness file path
set: {supersetWorker: {healthCheck: {enabled: true}}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: '/tmp/celery_worker_ready'}
- it: SQLALCHEMY_TRACK_MODIFICATIONS defaults False
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'SQLALCHEMY_TRACK_MODIFICATIONS = False'}
- it: renders feature flags
set: {featureFlags: {ALERT_REPORTS: true}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: '"ALERT_REPORTS": True'}
- it: SQLALCHEMY_TRACK_MODIFICATIONS user-set true renders Python True
set: {config: {SQLALCHEMY_TRACK_MODIFICATIONS: true}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'SQLALCHEMY_TRACK_MODIFICATIONS = True'}
- it: celeryConfig without imports emits a safe empty CELERY_IMPORTS (no AttributeError)
set: {config: {celeryConfig: {broker_url: "redis://x"}}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'CELERY_IMPORTS = \(\)'}
- notMatchRegex: {path: 'stringData["superset_config.py"]', pattern: 'CELERY_IMPORTS = CeleryConfig\.imports'}
- it: legacy db_type maps to the SQLAlchemy driver (deprecation path)
set: {supersetNode: {connections: {db_type: "postgresql+pg8000"}}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'SQLALCHEMY_DATABASE_URI = f"postgresql\+pg8000://'}
- it: init script single-quotes the admin username (no shell injection)
set: {init: {createAdmin: true, adminUser: {password: "x"}}}
asserts:
- matchRegex: {path: 'stringData["superset_init.sh"]', pattern: "grep -qF 'username:"}
- notMatchRegex: {path: 'stringData["superset_init.sh"]', pattern: 'create-admin[\s\S]*\|\| true'}
- it: sentinel disabled does not fail render
set: {cache: {enabled: true, sentinel: {enabled: false}}}
asserts:
- hasDocuments:
count: 1
- it: ssl certfile path is quoted
set: {cache: {enabled: true, ssl: {enabled: true, certfile: "/etc/ssl/cert.pem"}}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: '"CACHE_REDIS_SSL_CERTFILE": "/etc/ssl/cert.pem"'}
- it: explicit JWT cookie secure false is respected
set: {supersetWebsockets: {enabled: true}, config: {GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE: false}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = False'}
- it: celeryConfig non-string values render as valid Python via json.loads
set: {config: {celeryConfig: {broker_url: "redis://x", task_acks_late: true}}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'task_acks_late = json\.loads\('}
- it: featureFlags non-scalar values render as valid Python via json.loads
set: {featureFlags: {SOME_MAP: {nested: true}}}
asserts:
- matchRegex: {path: 'stringData["superset_config.py"]', pattern: '"SOME_MAP": json\.loads\('}

View File

@@ -0,0 +1,28 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
suite: deprecation path does not fail render
templates:
- secret-superset-config.yaml
tests:
- it: legacy supersetNode.connections still renders (no fail)
set: {supersetNode.connections.db_host: legacy}
asserts:
- hasDocuments: {count: 1}
- it: legacy root serviceAccountName still renders (no fail)
set: {serviceAccountName: my-sa}
asserts:
- hasDocuments: {count: 1}

View File

@@ -0,0 +1,63 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
suite: superset_init.sh rendering
templates:
- secret-superset-config.yaml
tests:
- it: renders superset db upgrade in init script
asserts:
- matchRegex:
path: 'stringData["superset_init.sh"]'
pattern: 'superset db upgrade'
- it: renders superset init in init script
asserts:
- matchRegex:
path: 'stringData["superset_init.sh"]'
pattern: 'superset init'
- it: renders db upgrade before admin creation
set:
init:
createAdmin: true
adminUser:
username: admin
firstname: Admin
lastname: User
email: admin@example.com
password: adminpass
asserts:
- matchRegex:
path: 'stringData["superset_init.sh"]'
pattern: 'superset db upgrade'
- matchRegex:
path: 'stringData["superset_init.sh"]'
pattern: 'superset init'
- matchRegex:
path: 'stringData["superset_init.sh"]'
pattern: 'fab create-admin'
- it: admin creation is idempotent and does not swallow real failures
set:
init:
createAdmin: true
adminUser:
password: "x"
asserts:
- matchRegex:
path: 'stringData["superset_init.sh"]'
pattern: 'superset fab list-users'
- notMatchRegex:
path: 'stringData["superset_init.sh"]'
pattern: 'create-admin.*\|\| true'

View File

@@ -0,0 +1,71 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
suite: secret-env schema + deprecation mapping
templates:
- secret-env.yaml
tests:
- it: emits DB_* and REDIS_* from new schema
set:
database: {host: pg.svc, port: 5432, user: su, password: pw, name: superset}
cache: {enabled: true, host: rd.svc, port: 6379, cacheDb: 1, celeryDb: 0}
asserts:
- equal: {path: 'stringData.DB_HOST', value: pg.svc}
- equal: {path: 'stringData.DB_PORT', value: "5432"}
- equal: {path: 'stringData.REDIS_HOST', value: rd.svc}
- it: maps ALL legacy supersetNode.connections.* keys (deprecation path, no fail)
set:
cache:
enabled: true
supersetNode:
connections:
db_host: legacy-pg
db_port: "5555"
db_user: legacy-user
db_pass: legacy-pass
db_name: legacy-name
redis_host: legacy-rd
redis_port: "6380"
redis_cache_db: "7"
redis_celery_db: "8"
redis_driver: rediss
asserts:
- equal: {path: 'stringData.DB_HOST', value: legacy-pg}
- equal: {path: 'stringData.DB_PORT', value: "5555"}
- equal: {path: 'stringData.DB_USER', value: legacy-user}
- equal: {path: 'stringData.DB_PASS', value: legacy-pass}
- equal: {path: 'stringData.DB_NAME', value: legacy-name}
- equal: {path: 'stringData.REDIS_HOST', value: legacy-rd}
- equal: {path: 'stringData.REDIS_PORT', value: "6380"}
- equal: {path: 'stringData.REDIS_DB', value: "7"}
- equal: {path: 'stringData.REDIS_CELERY_DB', value: "8"}
- equal: {path: 'stringData.REDIS_PROTO', value: rediss}
- it: new-schema values still win over legacy when both are set
set:
database: {host: new-host, port: 1234}
supersetNode:
connections: {db_host: old-host, db_port: "9999"}
asserts:
- equal: {path: 'stringData.DB_HOST', value: new-host}
- equal: {path: 'stringData.DB_PORT', value: "1234"}
- it: emits REDIS_DRIVER when cache.driver set
set: {cache: {enabled: true, driver: "rediss+managed"}}
asserts:
- equal: {path: 'stringData.REDIS_DRIVER', value: "rediss+managed"}
- it: omits redis vars when cache disabled
set: {cache.enabled: false}
asserts:
- notExists: {path: 'stringData.REDIS_HOST'}

View File

@@ -0,0 +1,28 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
suite: serviceAccountName resolution
templates:
- serviceaccount.yaml
tests:
- it: uses serviceAccount.name when set
set: {serviceAccount: {create: true, name: my-sa}}
asserts:
- equal: {path: 'metadata.name', value: my-sa}
- it: falls back to legacy root serviceAccountName (deprecation path)
set: {serviceAccount: {create: true}, serviceAccountName: legacy-sa}
asserts:
- equal: {path: 'metadata.name', value: legacy-sa}

View File

@@ -0,0 +1,30 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
suite: values defaults
templates:
- secret-env.yaml
tests:
- it: renders with new database defaults
set:
database.host: db.example
cache.enabled: true
asserts:
- hasDocuments:
count: 1
- equal:
path: 'stringData.DB_HOST'
value: db.example

View File

@@ -0,0 +1,140 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": true,
"properties": {
"database": {
"type": "object",
"additionalProperties": true,
"properties": {
"uri": {
"type": ["string", "null"]
},
"driver": {
"type": ["string", "null"]
},
"host": {
"type": ["string", "null"]
},
"port": {
"type": ["integer", "string", "null"]
},
"user": {
"type": ["string", "null"]
},
"password": {
"type": ["string", "null"]
},
"name": {
"type": ["string", "null"]
},
"ssl": {
"type": "object",
"additionalProperties": true,
"properties": {
"enabled": {
"type": "boolean"
},
"mode": {
"type": "string"
}
}
}
}
},
"cache": {
"type": "object",
"additionalProperties": true,
"properties": {
"enabled": {
"type": "boolean"
},
"driver": {
"type": "string"
},
"host": {
"type": ["string", "null"]
},
"port": {
"type": ["integer", "string", "null"]
},
"cacheDb": {
"type": ["integer", "string", "null"]
},
"celeryDb": {
"type": ["integer", "string", "null"]
},
"ssl": {
"type": "object",
"additionalProperties": true,
"properties": {
"enabled": {
"type": "boolean"
}
}
}
}
},
"cluster": {
"type": "object",
"additionalProperties": true,
"properties": {
"domain": {
"type": "string"
},
"databaseServiceName": {
"type": ["string", "null"]
},
"redisServiceName": {
"type": ["string", "null"]
},
"websocketServiceName": {
"type": ["string", "null"]
}
}
},
"config": {
"type": "object",
"additionalProperties": true
},
"featureFlags": {
"type": "object",
"additionalProperties": true
},
"serviceAccount": {
"type": "object",
"additionalProperties": true,
"properties": {
"create": {
"type": "boolean"
},
"name": {
"type": "string"
},
"annotations": {
"type": "object",
"additionalProperties": true
}
}
},
"globalPodAnnotations": {
"type": "object",
"additionalProperties": true
},
"image": {
"type": "object",
"additionalProperties": true,
"properties": {
"repository": {
"type": "string"
},
"tag": {
"type": ["string", "null"]
},
"pullPolicy": {
"type": "string"
}
}
}
}
}

View File

@@ -28,9 +28,30 @@ nameOverride: ~
# -- Provide a name to override the full names of resources
fullnameOverride: ~
# -- Kubernetes cluster configuration
# Used for constructing service URLs between chart components
cluster:
# -- Kubernetes cluster domain (default: .svc.cluster.local)
# Override if using a custom cluster domain
domain: ".svc.cluster.local"
# -- Database service name (default: {{ .Release.Name }}-postgresql)
# Override if using a different service name for the database
databaseServiceName: ~
# -- Redis service name (default: {{ .Release.Name }}-redis-headless)
# Override if using a different service name for Redis
redisServiceName: ~
# -- WebSocket service name (default: {{ .Release.Name }}-ws)
# Override if using a different service name for the WebSocket service
websocketServiceName: ~
# -- Labels to be added to all resources
extraLabels: {}
# -- 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
globalPodAnnotations: {}
# -- 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
runAsUser: 0
@@ -46,6 +67,8 @@ serviceAccountName: ~
serviceAccount:
# -- Create custom service account for Superset. If create: true and serviceAccountName is not provided, `superset.fullname` will be used.
create: false
# -- Service account name to use (if not specified, defaults to release name + chart name)
name: ""
annotations: {}
# -- Install additional packages and do any other bootstrap configuration in this script
@@ -244,6 +267,89 @@ hostAliases: []
# - nodns.my.lan
# ip: 18.27.36.45
# -- Database connection configuration for the Superset metadata database
database:
# -- Full database URI (overrides host/port/user/pass/name if set)
# Example: "postgresql+psycopg2://user:pass@host:5432/dbname"
uri: ~
# -- Database driver used when uri is not set (default: postgresql+psycopg2 when unset;
# legacy supersetNode.connections.db_type is honored when this is unset)
driver: ~
# -- Database host (default: {{ .Release.Name }}-postgresql)
host: ~
# -- Database port (default: 5432 when unset; legacy supersetNode.connections.db_port is honored)
port: ~
# -- Database user (default: superset, resolved via superset.db.user)
user: ~
# -- Database password (default: superset, resolved via superset.db.password)
# ⚠️ CHANGE THIS for production
password: ~
# -- Database name (default: superset, resolved via superset.db.name)
name: ~
# -- Database SSL configuration
ssl:
enabled: false
mode: require
# -- Redis cache configuration for Superset
# Redis is optional but recommended for caching and Celery.
# If redis.enabled (chart dependency) is true, defaults point to the chart's Redis instance.
cache:
# -- Enable Redis-based features (cache, Celery). Set to false to disable Redis usage entirely.
enabled: true
# -- Full Redis cache URL (overrides host/port/user/pass if set)
cacheUrl: ~
# -- Full Redis Celery URL (overrides host/port/user/pass if set)
celeryUrl: ~
# -- Redis host (default: {{ .Release.Name }}-redis-headless)
host: ~
# -- Redis port (default: 6379 when unset; legacy supersetNode.connections.redis_port is honored)
port: ~
# -- Redis user (optional, for Redis ACL)
user: ""
# -- Redis password
password: ~
# -- Redis database number for cache (default: 1 when unset; legacy redis_cache_db is honored)
cacheDb: ~
# -- Redis database number for Celery (default: 0 when unset; legacy redis_celery_db is honored)
celeryDb: ~
# -- Cache key prefix
keyPrefix: "superset_"
# -- Default cache timeout in seconds
defaultTimeout: 86400
# -- Results backend key prefix
resultsBackendKeyPrefix: "superset_results"
# -- Async queries configuration
asyncQueries:
keyPrefix: "qc-"
timeout: 86400
# -- Redis Sentinel configuration (optional)
sentinel: ~
# -- Redis SSL configuration
ssl:
enabled: false
ssl_cert_reqs: required
certfile: ~
keyfile: ~
ca_certs: ~
# -- Custom Redis driver (e.g. TLS/managed variants); overrides the redis proto in URLs when set.
# Ports the legacy supersetNode.connections.redis_driver escape hatch.
driver: ""
# -- Superset configuration properties
# Set any configuration property from superset/config.py here
# See https://github.com/apache/superset/blob/master/superset/config.py for all available options
config: {}
# SECRET_KEY: "your-secret-key-here"
# ROW_LIMIT: 50000
# DEBUG: false
# -- Feature flags configuration
# See https://github.com/apache/superset/blob/master/RESOURCES/FEATURE_FLAGS.md
featureFlags: {}
# ALERT_REPORTS: true
# DASHBOARD_RBAC: true
# Superset node configuration
supersetNode:
replicas:
@@ -270,28 +376,8 @@ supersetNode:
- "/bin/sh"
- "-c"
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec /usr/bin/run-server.sh"
connections:
# -- Change in case of bringing your own redis and then also set redis.enabled:false
redis_host: "{{ .Release.Name }}-redis-headless"
redis_port: "6379"
redis_user: ""
# redis_password: superset
redis_cache_db: "1"
redis_celery_db: "0"
# Or SSL port is usually 6380
# Update following for using Redis with SSL
redis_ssl:
enabled: false
ssl_cert_reqs: CERT_NONE
redis_driver: ""
# You need to change below configuration incase bringing own PostgresSQL instance and also set postgresql.enabled:false
# -- Database type for Superset metadata (Supported types: "postgresql", "mysql")
db_type: "postgresql"
db_host: "{{ .Release.Name }}-postgresql"
db_port: "5432"
db_user: superset
db_pass: superset
db_name: superset
# DEPRECATED: use database.* and cache.* instead (still honored if set). See UPGRADING.md
connections: {}
env: {}
# -- If true, forces deployment to reload on each upgrade
forceReload: false
@@ -508,6 +594,17 @@ supersetWorker:
startupProbe: {}
# -- No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic)
readinessProbe: {}
# -- Celery worker file-based health check (worker writes readiness/liveness files via signals;
# point supersetWorker.readinessProbe/livenessProbe at these files to use them)
healthCheck:
# -- Enable the file-based Celery worker health check
enabled: false
# -- Readiness file (created when the worker is ready, removed on shutdown)
readinessFile: "/tmp/celery_worker_ready"
# -- Liveness file (touched periodically by a heartbeat thread)
livenessFile: "/tmp/celery_worker_alive"
# -- Seconds between liveness heartbeats
livenessHeartbeatInterval: 10
# -- Set priorityClassName for supersetWorker pods
priorityClassName: ~
@@ -870,8 +967,11 @@ init:
requests:
cpu: "250m"
memory: "128Mi"
# -- A Superset init script
# @default -- a script to create admin user and initialize roles
# -- DEPRECATED: this field is no longer used by the chart.
# The init script is rendered entirely from the internal `superset.initScript` template
# (which runs `superset db upgrade`, `superset init`, admin creation, and examples).
# Any customization placed here is silently ignored. See UPGRADING.md.
# @default -- unused; kept for backwards-compatibility only
initscript: |-
#!/bin/sh
set -eu

View File

@@ -44,14 +44,14 @@ dependencies = [
# explicitly to keep fresh ``pip install apache-superset`` working
# without the ``base.txt`` lock file (#40962).
"cachetools>=6.2.1, <7",
"celery>=5.3.6, <6.0.0",
"celery>=5.6.3, <6.0.0",
"click>=8.4.0",
"click-option-group",
"colorama",
"flask-cors>=6.0.5, <7.0",
"croniter>=6.2.2",
"cron-descriptor",
"cryptography>=48.0.0, <49.0.0",
"cryptography>=49.0.0, <50.0.0",
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <4.0.0",
"flask-appbuilder>=5.2.2, <6.0.0",
@@ -106,8 +106,9 @@ dependencies = [
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=4.1.1",
"slack_sdk>=3.42.0, <4",
"slack_sdk>=3.43.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy-continuum>=1.6.0, <2.0.0",
"sqlalchemy-utils>=0.42.1, <0.43", # expanding lowerbound to work with pydoris
"sqlglot>=30.8.0, <31",
# newer pandas needs 0.9+
@@ -127,7 +128,7 @@ aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.19.1",
"sqlalchemy-bigquery>=1.17.0",
"google-cloud-bigquery>=3.10.0",
"google-cloud-bigquery>=3.42.1",
]
clickhouse = ["clickhouse-connect>=1.1.1, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
@@ -137,7 +138,7 @@ d1 = [
"sqlalchemy-d1>=0.1.0",
"dbapi-d1>=0.1.0",
]
databend = ["databend-sqlalchemy>=0.3.2, <1.0"]
databend = ["databend-sqlalchemy>=0.5.5, <1.0"]
databricks = [
"databricks-sql-connector==4.2.6",
"databricks-sqlalchemy==1.0.5",
@@ -160,11 +161,11 @@ fastmcp = [
# heuristic that under-counts JSON-heavy MCP responses.
"tiktoken>=0.13.0,<1.0",
]
firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
firebird = ["sqlalchemy-firebird>=0.8.0, <2.2"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=26.4.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
hana = ["hdbcli==2.28.21", "sqlalchemy_hana==0.4.0"]
hana = ["hdbcli==2.28.21", "sqlalchemy_hana==3.0.3"]
hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7.0",
@@ -172,7 +173,7 @@ hive = [
"thrift>=0.23.0, <1.0.0",
"thrift_sasl>=0.4.3, < 1.0.0",
]
impala = ["impyla>0.16.2, <0.23"]
impala = ["impyla>=0.24.0, <0.25"]
kusto = ["sqlalchemy-kusto>=3.1.2, <4"]
kylin = ["kylinpy>=2.8.4, <2.9"]
mssql = ["pymssql>=2.3.13, <3"]
@@ -180,15 +181,15 @@ mssql = ["pymssql>=2.3.13, <3"]
motherduck = ["apache-superset[duckdb]"]
mysql = ["mysqlclient>=2.2.8, <3"]
ocient = [
"sqlalchemy-ocient>=1.0.0",
"sqlalchemy-ocient>=1.0.0, <4",
"pyocient>=1.0.15, <4",
"shapely",
"geojson",
]
oracle = ["oracledb>=2.0.0, <5"]
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
parseable = ["sqlalchemy-parseable>=0.1.6,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.60.0, <2"]
playwright = ["playwright>=1.61.0, <2"]
postgres = ["psycopg2-binary==2.9.12"]
presto = ["pyhive[presto]>=0.6.5"]
trino = ["trino>=0.337.0"]
@@ -196,7 +197,7 @@ prophet = ["prophet>=1.1.6, <2"]
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
risingwave = ["sqlalchemy-risingwave"]
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.10.2, <2"]
sqlite = ["syntaqlite>=0.1.0,<0.5.0"]
spark = [
@@ -209,7 +210,7 @@ tdengine = [
"taospy>=2.8.9",
"taos-ws-py>=0.6.9"
]
teradata = ["teradatasql>=16.20.0.23"]
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"]
@@ -235,7 +236,7 @@ development = [
"pyfakefs",
"pyinstrument>=5.1.2,<6",
"pylint",
"pytest<8.0.0", # hairy issue with pytest >=8 where current_app proxies are not set in time
"pytest<10.0.0", # cap below the next major until validated; the earlier <8 pin (current_app proxy timing) no longer reproduces
"pytest-asyncio",
"pytest-cov",
"pytest-mock",

View File

@@ -26,7 +26,7 @@ filelock>=3.20.3,<4.0.0
brotli>=1.2.0,<2.0.0
numexpr>=2.9.0
# Security: CVE-2026-34073 (MEDIUM) - Improper Certificate Validation
cryptography>=48.0.0,<49.0.0
cryptography>=49.0.0,<50.0.0
# Security: Snyk - XSS vulnerability in Mako templates
mako>=1.3.11,<2.0.0
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers

View File

@@ -48,7 +48,7 @@ cachetools==6.2.1
# via apache-superset (pyproject.toml)
cattrs==25.1.1
# via requests-cache
celery==5.5.2
celery==5.6.3
# via apache-superset (pyproject.toml)
certifi==2026.5.20
# via
@@ -86,7 +86,7 @@ cron-descriptor==1.4.5
# via apache-superset (pyproject.toml)
croniter==6.2.2
# via apache-superset (pyproject.toml)
cryptography==48.0.1
cryptography==49.0.0
# via
# -r requirements/base.in
# apache-superset (pyproject.toml)
@@ -207,7 +207,7 @@ jsonschema-specifications==2025.4.1
# via
# jsonschema
# openapi-schema-validator
kombu==5.5.3
kombu==5.6.2
# via celery
limits==5.1.0
# via flask-limiter
@@ -272,6 +272,7 @@ packaging==25.0
# apispec
# deprecation
# gunicorn
# kombu
# limits
# shillelagh
pandas==2.1.4
@@ -323,7 +324,7 @@ pyjwt==2.13.0
# redis
pynacl==1.6.2
# via paramiko
pyopenssl==26.2.0
pyopenssl==26.3.0
# via
# -r requirements/base.in
# shillelagh
@@ -389,7 +390,7 @@ six==1.17.0
# python-dateutil
# rfc3339-validator
# wtforms-json
slack-sdk==3.42.0
slack-sdk==3.43.0
# via apache-superset (pyproject.toml)
sniffio==1.3.1
# via trio
@@ -404,7 +405,10 @@ sqlalchemy==1.4.54
# flask-sqlalchemy
# marshmallow-sqlalchemy
# shillelagh
# sqlalchemy-continuum
# sqlalchemy-utils
sqlalchemy-continuum==1.6.0
# via apache-superset (pyproject.toml)
sqlalchemy-utils==0.42.1
# via
# apache-superset (pyproject.toml)
@@ -444,6 +448,8 @@ tzdata==2025.2
# via
# kombu
# pandas
tzlocal==5.4.4
# via celery
url-normalize==2.2.1
# via requests-cache
urllib3==2.7.0

View File

@@ -108,7 +108,7 @@ cattrs==25.1.1
# via
# -c requirements/base-constraint.txt
# requests-cache
celery==5.5.2
celery==5.6.3
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -178,7 +178,7 @@ croniter==6.2.2
# via
# -c requirements/base-constraint.txt
# apache-superset
cryptography==48.0.1
cryptography==49.0.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -360,7 +360,7 @@ google-auth-oauthlib==1.2.1
# via
# pandas-gbq
# pydata-google-auth
google-cloud-bigquery==3.27.0
google-cloud-bigquery==3.42.1
# via
# apache-superset
# pandas-gbq
@@ -503,7 +503,7 @@ keyring==25.7.0
# via py-key-value-aio
kiwisolver==1.4.7
# via matplotlib
kombu==5.5.3
kombu==5.6.2
# via
# -c requirements/base-constraint.txt
# celery
@@ -635,6 +635,7 @@ packaging==25.0
# fastmcp-slim
# google-cloud-bigquery
# gunicorn
# kombu
# limits
# matplotlib
# pytest
@@ -790,9 +791,10 @@ pynacl==1.6.2
# via
# -c requirements/base-constraint.txt
# paramiko
pyopenssl==26.2.0
pyopenssl==26.3.0
# via
# -c requirements/base-constraint.txt
# google-auth
# shillelagh
pyparsing==3.2.3
# via
@@ -957,7 +959,7 @@ six==1.17.0
# python-dateutil
# rfc3339-validator
# wtforms-json
slack-sdk==3.42.0
slack-sdk==3.43.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -982,9 +984,14 @@ sqlalchemy==1.4.54
# marshmallow-sqlalchemy
# shillelagh
# sqlalchemy-bigquery
# sqlalchemy-continuum
# sqlalchemy-utils
sqlalchemy-bigquery==1.17.0
# via apache-superset
sqlalchemy-continuum==1.6.0
# via
# -c requirements/base-constraint.txt
# apache-superset
sqlalchemy-utils==0.42.1
# via
# -c requirements/base-constraint.txt
@@ -1071,8 +1078,11 @@ tzdata==2025.2
# -c requirements/base-constraint.txt
# kombu
# pandas
tzlocal==5.2
# via trino
tzlocal==5.4.4
# via
# -c requirements/base-constraint.txt
# celery
# trino
uncalled-for==0.2.0
# via fastmcp-slim
url-normalize==2.2.1

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Stamp do-not-translate msgids in a .pot with an extracted-comment marker.
For every msgid listed in ``superset/translations/do-not-translate.txt`` that is
present in the target .pot, add a ``#. do-not-translate`` extracted
comment. gettext extracted comments (``#.``) propagate from the .pot into every
language .po on ``pybabel update``, so the do-not-translate status stays
consistent across all catalogs from a single registry.
Run from ``babel_update.sh`` after the .pot is extracted and normalized (and
before ``pybabel update``). Idempotent: re-running makes no further changes.
Usage:
python scripts/translations/apply_do_not_translate.py [POT_PATH]
# POT_PATH defaults to superset/translations/messages.pot
"""
from __future__ import annotations
import sys
from pathlib import Path
# The standardized extracted-comment marker. Kept in sync with backfill_po.py.
MARKER: str = "do-not-translate"
_MARKER_LINE: str = f"#. {MARKER}"
TRANSLATIONS_DIR: Path = (
Path(__file__).parent.parent.parent / "superset" / "translations"
)
DEFAULT_POT: Path = TRANSLATIONS_DIR / "messages.pot"
REGISTRY: Path = TRANSLATIONS_DIR / "do-not-translate.txt"
def load_registry(path: Path = REGISTRY) -> set[str]:
"""Return the set of do-not-translate msgids (skips comments/blank lines).
Each line is stripped before the blank/comment check, so trailing
whitespace or an indented comment never yields a msgid that fails to match
the .pot.
"""
if not path.exists():
return set()
entries: set[str] = set()
for raw_line in path.read_text(encoding="utf-8").splitlines():
line: str = raw_line.strip()
if line and not line.startswith("#"):
entries.add(line)
return entries
def _escape(msgid: str) -> str:
"""Escape a msgid the way gettext writes it on a `msgid "..."` line."""
return msgid.replace("\\", "\\\\").replace('"', '\\"')
def apply_markers(pot_path: Path, registry: set[str]) -> int:
"""Insert the marker comment above each registry msgid via text edit.
Text manipulation (rather than a polib round-trip) preserves the .pot's
exact wrapping/layout, so the only change is the added marker lines.
Idempotent. Returns the number of entries newly marked.
"""
lines: list[str] = pot_path.read_text(encoding="utf-8").split("\n")
targets: set[str] = {f'msgid "{_escape(m)}"' for m in registry}
out: list[str] = []
changed: int = 0
for line in lines:
if line in targets and (not out or out[-1] != _MARKER_LINE):
# `#.` extracted comments precede `msgid`; these registry entries are
# bare single-line msgids, so inserting directly above is correct.
out.append(_MARKER_LINE)
changed += 1
out.append(line)
if changed:
pot_path.write_text("\n".join(out), encoding="utf-8")
return changed
def main() -> None:
"""Stamp the marker onto the target .pot from the registry."""
pot_path: Path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_POT
if not pot_path.exists():
print(f"POT file not found: {pot_path}", file=sys.stderr)
sys.exit(1)
# Fail fast if the registry file is absent: babel_update.sh depends on this
# step to stamp the .pot, and continuing would silently publish catalogs
# without any do-not-translate markers. An existing-but-empty registry is a
# valid state (nothing to mark), so only a missing file is an error.
if not REGISTRY.exists():
print(
f"do-not-translate registry not found at {REGISTRY}; refusing to "
"produce unmarked translation artifacts.",
file=sys.stderr,
)
sys.exit(1)
registry: set[str] = load_registry()
if not registry:
print(
f"do-not-translate registry {REGISTRY} is empty; nothing to mark.",
file=sys.stderr,
)
return
changed: int = apply_markers(pot_path, registry)
print(
f"do-not-translate: marked {changed} new entr(y/ies) with {MARKER} "
f"in {pot_path.name} ({len(registry)} msgids in registry).",
file=sys.stderr,
)
if __name__ == "__main__":
main()

View File

@@ -55,6 +55,14 @@ msgcat --sort-by-msgid --no-wrap --no-location superset/translations/messages.po
cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \
&& mv messages.pot.tmp superset/translations/messages.pot
# Stamp do-not-translate msgids (superset/translations/do-not-translate.txt) with
# a `#. do-not-translate` extracted comment. Extracted comments
# propagate from the .pot into every catalog on the `pybabel update` below, so
# the do-not-translate status stays consistent across all languages.
# Fail fast: without this guard the script would continue past a marker-stamping
# failure and `pybabel update` would publish catalogs missing the markers.
python scripts/translations/apply_do_not_translate.py superset/translations/messages.pot || exit 1
# --no-fuzzy-matching: when a *new* source string is added, Babel's fuzzy
# matcher otherwise guesses a "close" existing translation and marks it
# `#, fuzzy` in every language catalog. Those guesses are (a) usually wrong

View File

@@ -152,6 +152,60 @@ def _is_missing(entry: polib.POEntry) -> bool:
return not entry.msgstr
# Canonical registry of msgids that must never be machine-translated: literal
# tokens compared against source (SQL keywords, confirmation words), enum values
# (d3 interpolation modes), icon names (e.g. "bolt" -> the ⚡ Explore control
# icon), API field names, code constants, and example placeholders. Translating
# them can break icon lookups, enum matching, or API contracts, or is simply
# meaningless (proper nouns, example values). apply_do_not_translate.py stamps
# these msgids in messages.pot with a `#. do-not-translate`
# extracted comment that propagates to every catalog on `pybabel update`.
DO_NOT_TRANSLATE_REGISTRY: Path = TRANSLATIONS_DIR / "do-not-translate.txt"
def _load_do_not_translate(path: Path = DO_NOT_TRANSLATE_REGISTRY) -> frozenset[str]:
"""Load the do-not-translate msgids (skips comment/blank lines).
Lines are stripped before the blank/comment checks, matching the parsing in
apply_do_not_translate.py, so trailing whitespace or an indented comment
never yields a msgid that fails to match a catalog entry.
"""
if not path.exists():
return frozenset()
return frozenset(
stripped
for line in path.read_text(encoding="utf-8").splitlines()
if (stripped := line.strip()) and not stripped.startswith("#")
)
DO_NOT_TRANSLATE: frozenset[str] = _load_do_not_translate()
# An explicit do-not-translate marker on an entry, matched in either the
# extracted comment (`#. do-not-translate`, the standard propagated
# from the .pot) or a translator comment (e.g. the ru catalog's legacy
# "# Не переводить"). Honored so a human's deliberate decision is never
# overridden even if a msgid is missing from the registry.
_DO_NOT_TRANSLATE_COMMENT: re.Pattern[str] = re.compile(
r"не\s+переводить|do[\s-]?not[\s-]?translate|don'?t\s+translate",
re.IGNORECASE,
)
def _is_do_not_translate(entry: polib.POEntry) -> bool:
"""Return True if an entry must be left for a human (never machine-filled).
Either its msgid is in the do-not-translate registry, or the entry carries
an explicit do-not-translate marker in its extracted or translator comment.
"""
if entry.msgid in DO_NOT_TRANSLATE:
return True
return any(
comment and _DO_NOT_TRANSLATE_COMMENT.search(comment)
for comment in (entry.comment, entry.tcomment)
)
def _context_langs(
item: dict[str, Any], index: dict[str, Any], target_lang: str
) -> list[str]:
@@ -647,6 +701,15 @@ def backfill(
missing: list[polib.POEntry] = [e for e in cat if e.msgid and _is_missing(e)]
print(f"Found {len(missing)} untranslated entries for '{lang}'.", file=sys.stderr)
skipped_dnt: list[polib.POEntry] = [e for e in missing if _is_do_not_translate(e)]
if skipped_dnt:
missing = [e for e in missing if not _is_do_not_translate(e)]
print(
f"Skipping {len(skipped_dnt)} do-not-translate entries (literal "
f"tokens / translator-marked); they are left untranslated.",
file=sys.stderr,
)
if min_context > 0:
before = len(missing)
missing = [

View File

@@ -42,7 +42,7 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"flask-appbuilder>=5.0.2,<6",
"flask-appbuilder>=5.2.0,<6",
"isodate>=0.7.0",
"pyarrow>=16.0.0",
"pydantic>=2.8.0",

View File

@@ -42,7 +42,10 @@ from superset_core.common.models import (
Dashboard,
Database,
Dataset,
Group,
KeyValue,
Role,
Subject,
Tag,
User,
)
@@ -190,6 +193,34 @@ class UserDAO(BaseDAO[User]):
id_column_name = "id"
class RoleDAO(BaseDAO[Role]):
"""
Abstract Role DAO interface.
Host implementations will replace this class during initialization
with a concrete implementation providing actual functionality.
"""
# Class variables that will be set by host implementation
model_cls = None
base_filter = None
id_column_name = "id"
class GroupDAO(BaseDAO[Group]):
"""
Abstract Group DAO interface.
Host implementations will replace this class during initialization
with a concrete implementation providing actual functionality.
"""
# Class variables that will be set by host implementation
model_cls = None
base_filter = None
id_column_name = "id"
class TagDAO(BaseDAO[Tag]):
"""
Abstract Tag DAO interface.
@@ -218,13 +249,16 @@ class KeyValueDAO(BaseDAO[KeyValue]):
id_column_name = "id"
__all__ = [
"BaseDAO",
"DatasetDAO",
"DatabaseDAO",
"ChartDAO",
"DashboardDAO",
"UserDAO",
"TagDAO",
"KeyValueDAO",
]
class SubjectDAO(BaseDAO[Subject]):
"""
Abstract Subject DAO interface.
Host implementations will replace this class during initialization
with a concrete implementation providing actual functionality.
"""
# Class variables that will be set by host implementation
model_cls = None
base_filter = None
id_column_name = "id"
uuid_column_name = "uuid"

View File

@@ -285,6 +285,38 @@ class User(CoreModel):
active: bool
class Role(CoreModel):
"""
Abstract Role model interface.
Host implementations will replace this class during initialization
with concrete implementation providing actual functionality.
"""
__abstract__ = True
# Type hints for expected attributes (no actual field definitions)
id: int
name: str | None
class Group(CoreModel):
"""
Abstract Group model interface.
Host implementations will replace this class during initialization
with concrete implementation providing actual functionality.
"""
__abstract__ = True
# Type hints for expected attributes (no actual field definitions)
id: int
name: str | None
label: str | None
description: str | None
class Tag(CoreModel):
"""
Abstract Tag model interface.
@@ -320,6 +352,33 @@ class KeyValue(CoreModel):
changed_by_fk: int | None
class Subject(CoreModel):
"""
Abstract Subject model interface.
A Subject is a unified representation of a User, Role, or Group. Host
implementations will replace this class during initialization with a
concrete implementation providing actual functionality.
"""
__abstract__ = True
# Type hints for expected attributes (no actual field definitions)
id: int
uuid: UUID | None
label: str | None
secondary_label: str | None
active: bool | None
extra_search: str | None
type: int
# Convenience references to the underlying principal. Exactly one is set,
# matching ``type`` (User, Role, or Group respectively).
user: User | None
role: Role | None
group: Group | None
def get_session() -> scoped_session:
"""
Retrieve the SQLAlchemy session to directly interface with the
@@ -339,8 +398,11 @@ __all__ = [
"Chart",
"Dashboard",
"User",
"Role",
"Group",
"Tag",
"KeyValue",
"Subject",
"CoreModel",
"get_session",
]

View File

@@ -158,7 +158,7 @@ class Filter:
type: PredicateType
column: Dimension | Metric | None
operator: Operator
value: FilterValues | frozenset[FilterValues]
value: FilterValues | tuple[FilterValues, ...] | frozenset[FilterValues]
class OrderDirection(enum.Enum):

View File

@@ -57,9 +57,9 @@ export function saveChartToDashboard(chartName: string, dashboardName: string) {
'.ant-select-selection-search-input[aria-label*="Select a dashboard"]',
)
.type(dashboardName, { force: true });
cy.wrap($modal)
.find(`.ant-select-item-option[title="${dashboardName}"]`)
.click();
cy.get(`.ant-select-item-option[title="${dashboardName}"]`).click();
cy.getBySel('btn-modal-save').click();
cy.wait('@update');
});

View File

@@ -2,7 +2,6 @@
{
"slice_name": "1 - Sample chart",
"description": "chart description",
"owners": [1],
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
@@ -12,7 +11,6 @@
{
"slice_name": "2 - Sample chart",
"description": "chart description",
"owners": [1],
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
@@ -22,7 +20,6 @@
{
"slice_name": "3 - Sample chart",
"description": "chart description",
"owners": [1],
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
@@ -32,7 +29,6 @@
{
"slice_name": "4 - Sample chart",
"description": "chart description",
"owners": [1],
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,

View File

@@ -72,7 +72,10 @@ module.exports = {
],
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
transformIgnorePatterns: [
'node_modules/(?!@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact)',
// @ant-design/colors and @ant-design/fast-color are allowed through because
// @ant-design/icons >= 6.3 deep-imports the ESM build of @ant-design/colors
// from its CJS output, so babel-jest must transform those files.
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact)',
],
preset: 'ts-jest',
transform: {

File diff suppressed because it is too large Load Diff

View File

@@ -107,6 +107,7 @@
"@deck.gl/mesh-layers": "~9.2.5",
"@deck.gl/react": "~9.2.5",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/cache": "^11.4.0",
@@ -164,8 +165,8 @@
"@visx/scale": "^4.0.0",
"@visx/tooltip": "^4.0.0",
"@visx/xychart": "^4.0.0",
"ag-grid-community": "35.3.1",
"ag-grid-react": "35.3.1",
"ag-grid-community": "36.0.0",
"ag-grid-react": "36.0.0",
"antd": "^5.26.0",
"chrono-node": "^2.9.1",
"classnames": "^2.2.5",
@@ -176,7 +177,7 @@
"dom-to-pdf": "^0.3.2",
"echarts": "^6.1.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.3.5",
"fs-extra": "^11.3.6",
"fuse.js": "^7.4.2",
"geolib": "^3.3.14",
"geostyler": "^18.6.0",
@@ -185,7 +186,7 @@
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.9.0",
"immer": "^11.1.8",
"immer": "^11.1.9",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
"js-levenshtein": "^1.1.6",
@@ -271,7 +272,7 @@
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.43",
"@swc/plugin-emotion": "^14.14.0",
"@swc/plugin-emotion": "^14.14.1",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.9.1",
@@ -285,7 +286,7 @@
"@types/json-bigint": "^1.0.4",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
"@types/node": "^26.0.1",
"@types/node": "^26.1.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react-loadable": "^5.5.11",
@@ -298,13 +299,13 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.61.0",
"@typescript-eslint/eslint-plugin": "^8.62.1",
"@typescript-eslint/parser": "^8.62.1",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"baseline-browser-mapping": "^2.10.40",
"baseline-browser-mapping": "^2.10.41",
"cheerio": "1.2.0",
"concurrently": "^10.0.3",
"copy-webpack-plugin": "^14.0.0",
@@ -314,7 +315,6 @@
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-cypress": "^3.6.0",
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"eslint-plugin-import": "^2.32.0",
@@ -324,7 +324,7 @@
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
"eslint-plugin-storybook": "10.4.5",
"eslint-plugin-storybook": "10.4.6",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -344,9 +344,9 @@
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.6.1",
"open-cli": "^9.0.0",
"oxlint": "^1.71.0",
"oxlint": "^1.72.0",
"po2json": "^0.4.5",
"prettier": "3.9.3",
"prettier": "3.9.4",
"prettier-plugin-packagejson": "^3.0.2",
"process": "^0.11.10",
"react-dnd-test-backend": "^16.0.1",
@@ -361,7 +361,7 @@
"swc-loader": "^0.2.7",
"ts-jest": "^29.4.11",
"tscw-config": "^1.1.2",
"tsx": "^4.22.4",
"tsx": "^4.22.5",
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"vm-browserify": "^1.1.2",

View File

@@ -35,7 +35,7 @@
},
"devDependencies": {
"cross-env": "^10.1.0",
"fs-extra": "^11.3.5",
"fs-extra": "^11.3.6",
"jest": "^30.4.2",
"yeoman-test": "^11.6.0"
},

View File

@@ -258,6 +258,16 @@ export interface SupersetSpecificTokens {
dashboardTileBorder?: string;
dashboardTileBorderRadius?: number;
dashboardTileBoxShadow?: string;
/**
* Results grid customization tokens.
* Control the appearance of AG Grid-backed result tables (e.g. SQL Lab).
*/
resultsGridRowHeight?: number;
resultsGridHeaderFontWeight?: number;
resultsGridHeaderFontSize?: number;
resultsGridBorderRadius?: number;
resultsGridNoStriping?: boolean;
}
/**

View File

@@ -25,18 +25,35 @@ export { default as __hack_reexport_jed } from './jed';
* Superset supported languages.
*/
export type Locale =
| 'ar'
| 'ca'
| 'cs'
| 'de'
| 'en'
| 'es'
| 'fa'
| 'fi'
| 'fr'
| 'it'
| 'ja'
| 'ko'
| 'lv'
| 'mi'
| 'nl'
| 'pl'
| 'pt'
| 'pt_BR'
| 'ro'
| 'ru'
| 'sk'
| 'sl'
| 'sr'
| 'sr_Latn'
| 'th'
| 'tr'
| 'uk'
| 'zh'
| 'zh_TW'; // supported locales in Superset
| 'zh_TW'; // every locale with a catalog in superset/translations
/**
* Language pack provided to `jed`.

View File

@@ -138,7 +138,7 @@ export const TestDataset: Dataset = {
],
name: 'public.birth_names',
order_by_choices: [],
owners: [
editors: [
{
first_name: 'admin',
id: 1,

View File

@@ -67,7 +67,7 @@ export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
[...metricOffsetMap.entries()].forEach(
([metricWithOffset, metricOnly]) => {
const offsetLabel = timeOffsets.find(offset =>
metricWithOffset.includes(offset),
metricWithOffset.endsWith(`${TIME_COMPARISON_SEPARATOR}${offset}`),
);
renamePairs.push([
formData.comparison_type === ComparisonType.Values

View File

@@ -26,7 +26,7 @@ export const getTimeOffset = (
timeCompare.find(
timeOffset =>
// offset is represented as <offset>, group by list
series.name.includes(`${timeOffset},`) ||
series.name.startsWith(`${timeOffset},`) ||
// offset is represented as <metric>__<offset>
series.name.includes(`__${timeOffset}`) ||
// offset is represented as <metric>, <offset>
@@ -50,7 +50,9 @@ export const getOriginalSeries = (
// offset in the middle: <metric>, <offset>, <dimension>
result = result.replace(`, ${compare},`, ',');
// offset at start: <offset>, <dimension>
result = result.replace(`${compare},`, '');
if (result.startsWith(`${compare},`)) {
result = result.slice(`${compare},`.length);
}
// offset with double underscore: <metric>__<offset>
result = result.replace(`__${compare}`, '');
// offset at end: <metric>, <offset>

View File

@@ -86,7 +86,7 @@ export interface Dataset {
name?: string;
description: string | null;
uid?: string;
owners?: Owner[];
editors?: Owner[];
filter_select?: boolean;
filter_select_enabled?: boolean;
column_names?: string[];

View File

@@ -303,6 +303,30 @@ test('should add renameOperator if multiple metrics exist', () => {
});
});
test('should correctly match offsets that share a numeric prefix', () => {
expect(
renameOperator(
{
...formData,
comparison_type: ComparisonType.Values,
time_compare: ['1 year ago', '11 year ago'],
},
queryObject,
),
).toEqual({
operation: 'rename',
options: {
columns: {
'count(*)__1 year ago': '1 year ago',
'count(*)__11 year ago': '11 year ago',
},
inplace: true,
level: 0,
},
});
});
test('should remove renameOperator', () => {
expect(
renameOperator(

View File

@@ -114,3 +114,26 @@ test('hasTimeOffset returns false when series name is not a string', () => {
const timeCompare = ['1 year ago'];
expect(hasTimeOffset(series, timeCompare)).toBe(false);
});
test('getTimeOffset correctly matches offsets that share a numeric prefix', () => {
const timeCompare = ['1 year ago', '11 year ago'];
expect(
getTimeOffset({ name: '11 year ago, Alexander' }, timeCompare),
).toEqual('11 year ago');
expect(getTimeOffset({ name: '1 year ago, Alexander' }, timeCompare)).toEqual(
'1 year ago',
);
expect(getTimeOffset({ name: 'Births__11 year ago' }, timeCompare)).toEqual(
'11 year ago',
);
});
test('getOriginalSeries correctly strips offsets that share a numeric prefix', () => {
const timeCompare = ['1 year ago', '11 year ago'];
expect(getOriginalSeries('11 year ago, Alexander', timeCompare)).toEqual(
'Alexander',
);
expect(getOriginalSeries('1 year ago, Alexander', timeCompare)).toEqual(
'Alexander',
);
});

View File

@@ -24,15 +24,15 @@
"lib"
],
"dependencies": {
"@ant-design/icons": "^6.2.5",
"@ant-design/icons": "^6.3.2",
"@apache-superset/core": "*",
"@babel/runtime": "^7.29.7",
"@braintree/sanitize-url": "^7.1.2",
"@types/json-bigint": "^1.0.4",
"@visx/responsive": "^4.0.0",
"ace-builds": "^1.44.0",
"ag-grid-community": "35.3.1",
"ag-grid-react": "35.3.1",
"ag-grid-community": "36.0.0",
"ag-grid-react": "36.0.0",
"brace": "^0.11.1",
"classnames": "^2.5.1",
"core-js": "^3.49.0",
@@ -55,7 +55,7 @@
"react-draggable": "^4.7.0",
"react-error-boundary": "^6.1.2",
"react-js-cron": "^5.2.0",
"react-markdown": "^9.0.7",
"react-markdown": "^10.1.0",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^16.1.1",
"react-ultimate-pagination": "^1.3.2",
@@ -73,12 +73,12 @@
"@emotion/styled": "^11.14.1",
"@types/d3-format": "^3.0.4",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-scale": "^2.1.1",
"@types/d3-scale": "^4.0.9",
"@types/d3-time": "^3.0.4",
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.1",
"@types/lodash": "^4.17.24",
"@types/node": "^26.0.1",
"@types/node": "^26.1.0",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",

View File

@@ -154,18 +154,13 @@ class CategoricalColorScale extends ExtensibleFunction {
this.incrementColorRange();
}
if (
// feature flag to be deprecated (will become standard behaviour)
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
this.isColorUsed(color)
) {
if (this.isColorUsed(color)) {
// fallback to least used color
color = this.getNextAvailableColor(cleanedValue, color);
}
}
if (
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
source === LabelsColorMapSource.Dashboard &&
(forcedColor || isExistingLabel)
) {
@@ -396,17 +391,24 @@ class CategoricalColorScale extends ExtensibleFunction {
/**
* Returns the current unknown value, which defaults to "implicit".
*/
unknown(): string | { name: 'implicit' };
unknown(): { name: 'implicit' };
/**
* Sets the output value of the scale for unknown input values and returns this scale.
* The implicit value enables implicit domain construction. scaleImplicit can be used as a convenience to set the implicit value.
*
* The signatures mirror d3's `ScaleOrdinal.unknown` so this class remains
* assignable to `ScaleOrdinal<{ toString(): string }, string>`.
*
* @param value Unknown value to be used or scaleImplicit to set implicit scale generation.
*/
unknown(value: string | { name: 'implicit' }): this;
unknown<NewUnknown>(
value: NewUnknown,
): NewUnknown extends { name: 'implicit' }
? ScaleOrdinal<{ toString(): string }, string>
: ScaleOrdinal<{ toString(): string }, string, NewUnknown>;
unknown(value?: string | { name: 'implicit' }): unknown {
unknown(value?: unknown): unknown {
if (typeof value === 'undefined') {
return this.scale.unknown();
}

View File

@@ -91,6 +91,7 @@ import {
InfoCircleFilled,
InsertRowAboveOutlined,
InsertRowBelowOutlined,
LayoutOutlined,
LeftOutlined,
LineChartOutlined,
LineOutlined,
@@ -249,6 +250,7 @@ const AntdIcons = {
InfoCircleFilled,
InsertRowAboveOutlined,
InsertRowBelowOutlined,
LayoutOutlined,
LeftOutlined,
LineChartOutlined,
LineOutlined,

View File

@@ -84,15 +84,15 @@ const config = (contentType: ContentType) => {
),
};
case MetadataType.Owner:
case MetadataType.Editor:
return {
icon: Icons.UserOutlined,
title: contentType.createdBy,
tooltip: (
<div>
<Info header={t('Created by')} text={contentType.createdBy} />
{!!contentType.owners && (
<Info header={t('Owners')} text={contentType.owners} />
{!!contentType.editors && (
<Info header={t('Editors')} text={contentType.editors} />
)}
<Info header={t('Created on')} text={contentType.createdOn} />
</div>

View File

@@ -21,7 +21,7 @@ export enum MetadataType {
Dashboards = 'dashboards',
Description = 'description',
LastModified = 'lastModified',
Owner = 'owner',
Editor = 'editor',
Rows = 'rows',
Sql = 'sql',
Table = 'table',
@@ -48,10 +48,10 @@ export type LastModified = {
onClick?: (type: string) => void;
};
export type Owner = {
type: MetadataType.Owner;
export type Editor = {
type: MetadataType.Editor;
createdBy: string;
owners?: string[] | string;
editors?: string[] | string;
createdOn: string;
onClick?: (type: string) => void;
};
@@ -81,4 +81,4 @@ export type Tags = {
};
export type ContentType =
Dashboards | Description | LastModified | Owner | Rows | Sql | Table | Tags;
Dashboards | Description | LastModified | Editor | Rows | Sql | Table | Tags;

View File

@@ -27,7 +27,7 @@ export default {
docs: {
description: {
component:
'MetadataBar displays a row of metadata items (SQL info, owners, last modified, tags, dashboards, etc.) that collapse responsively based on available width.',
'MetadataBar displays a row of metadata items (SQL info, editors, last modified, tags, dashboards, etc.) that collapse responsively based on available width.',
},
},
},
@@ -75,9 +75,9 @@ Basic.args = {
title: 'Click to view query',
},
{
type: MetadataType.Owner,
type: MetadataType.Editor,
createdBy: 'Jane Smith',
owners: ['John Doe', 'Mary Wilson'],
editors: ['John Doe', 'Mary Wilson'],
createdOn: A_WEEK_AGO,
},
{
@@ -119,9 +119,9 @@ InteractiveMetadataBar.args = {
title: 'Click to view query',
},
{
type: MetadataType.Owner,
type: MetadataType.Editor,
createdBy: 'Jane Smith',
owners: ['John Doe', 'Mary Wilson'],
editors: ['John Doe', 'Mary Wilson'],
createdOn: A_WEEK_AGO,
},
{
@@ -149,9 +149,9 @@ InteractiveMetadataBar.parameters = {
items: [
{ type: 'sql', title: 'Click to view query' },
{
type: 'owner',
type: 'editor',
createdBy: 'Jane Smith',
owners: ['John Doe', 'Mary Wilson'],
editors: ['John Doe', 'Mary Wilson'],
createdOn: 'a week ago',
},
{
@@ -171,9 +171,9 @@ InteractiveMetadataBar.parameters = {
const items = [
{ type: 'sql', title: 'Click to view query' },
{
type: 'owner',
type: 'editor',
createdBy: 'Jane Smith',
owners: ['John Doe', 'Mary Wilson'],
editors: ['John Doe', 'Mary Wilson'],
createdOn: 'a week ago',
},
{
@@ -190,7 +190,7 @@ InteractiveMetadataBar.parameters = {
title: 'Minimal Metadata',
code: `function MinimalMetadata() {
const items = [
{ type: 'owner', createdBy: 'Admin', owners: ['Admin'], createdOn: 'yesterday' },
{ type: 'editor', createdBy: 'Admin', editors: ['Admin'], createdOn: 'yesterday' },
{ type: 'lastModified', value: '2 hours ago', modifiedBy: 'Admin' },
];
return <MetadataBar items={items} />;
@@ -201,7 +201,7 @@ InteractiveMetadataBar.parameters = {
code: `function FullMetadata() {
const items = [
{ type: 'sql', title: 'SELECT * FROM ...' },
{ type: 'owner', createdBy: 'Jane Smith', owners: ['Jane Smith', 'John Doe', 'Bob Wilson'], createdOn: '2 weeks ago' },
{ type: 'editor', createdBy: 'Jane Smith', editors: ['Jane Smith', 'John Doe', 'Bob Wilson'], createdOn: '2 weeks ago' },
{ type: 'lastModified', value: '3 days ago', modifiedBy: 'John Doe' },
{ type: 'tags', values: ['production', 'finance', 'quarterly'] },
{ type: 'dashboards', title: 'Used in 12 dashboards' },

View File

@@ -36,7 +36,7 @@ const SQL_TITLE = 'Click to view query';
const TABLE_TITLE = 'database.schema.table';
const CREATED_BY = 'Jane Smith';
const MODIFIED_BY = 'Jane Smith';
const OWNERS = ['John Doe', 'Mary Wilson'];
const EDITORS = ['John Doe', 'Mary Wilson'];
const TAGS = ['management', 'research', 'poc'];
const A_WEEK_AGO = 'a week ago';
const TWO_DAYS_AGO = '2 days ago';
@@ -71,9 +71,9 @@ const ITEMS: ContentType[] = [
modifiedBy: MODIFIED_BY,
},
{
type: MetadataType.Owner,
type: MetadataType.Editor,
createdBy: CREATED_BY,
owners: OWNERS,
editors: EDITORS,
createdOn: A_WEEK_AGO,
},
{
@@ -211,15 +211,15 @@ test('correctly renders the last modified tooltip', async () => {
expect(within(tooltip).getByText(MODIFIED_BY)).toBeInTheDocument();
});
test('correctly renders the owner tooltip', async () => {
test('correctly renders the editor tooltip', async () => {
render(<MetadataBar items={ITEMS.slice(0, 4)} />);
await userEvent.hover(screen.getByText(CREATED_BY));
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(within(tooltip).getByText(CREATED_BY)).toBeInTheDocument();
expect(within(tooltip).getByText(A_WEEK_AGO)).toBeInTheDocument();
OWNERS.forEach(owner =>
expect(within(tooltip).getByText(owner)).toBeInTheDocument(),
EDITORS.forEach(editor =>
expect(within(tooltip).getByText(editor)).toBeInTheDocument(),
);
});

View File

@@ -54,7 +54,7 @@ in the code and should be specific for each metadata object.
Content types are predefined and consistent across the whole app. This means that
they will be displayed and behave in a consistent manner, keeping the same ordering,
information formatting, and interactions. For example, the Owner content type will always
have the same icon and when hovered it will present who created the entity, its current owners, and when the entity was created.
have the same icon and when hovered it will present who created the entity, its current editors, and when the entity was created.
To extend the list of content types, a developer needs to request the inclusion of the new type in the design system.
This process is important to make sure the new type is reviewed by the design team, improving Superset consistency.
@@ -125,7 +125,7 @@ export type LastModified = {
export type Owner = {
type: MetadataType.Owner;
createdBy: string;
owners: string[];
editors: string[];
createdOn: Date;
onClick?: (type: string) => void;
};

View File

@@ -34,6 +34,6 @@ export const ORDER = {
rows: 3,
tags: 4,
description: 5,
owner: 6,
editor: 6,
lastModified: 7,
};

View File

@@ -16,7 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen, userEvent } from '@superset-ui/core/spec';
import { render, screen, userEvent, fireEvent } from '@superset-ui/core/spec';
import { ModalTrigger } from '.';
const mockedProps = {
@@ -74,3 +75,42 @@ test('should render a modal after click', async () => {
await userEvent.click(screen.getByRole('button'));
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
test('stops propagation of navigation keys to parent elements', async () => {
const handleParentKeyDown = jest.fn();
render(
<div onKeyDown={handleParentKeyDown}>
<ModalTrigger
triggerNode={<span>Trigger</span>}
modalBody={<input data-test="modal-input" />}
/>
</div>,
);
await userEvent.click(screen.getByText('Trigger'));
const input = await screen.findByTestId('modal-input');
const blockedKeys = [
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowDown',
'Home',
'End',
];
for (const key of blockedKeys) {
handleParentKeyDown.mockClear();
fireEvent.keyDown(input, { key });
expect(handleParentKeyDown).not.toHaveBeenCalled();
}
const allowedKeys = ['Escape', 'Tab'];
for (const key of allowedKeys) {
handleParentKeyDown.mockClear();
fireEvent.keyDown(input, { key });
expect(handleParentKeyDown).toHaveBeenCalled();
}
});

View File

@@ -27,6 +27,15 @@ import {
import { Button } from '../Button';
import { Modal } from '../Modal';
const MENU_NAVIGATION_KEYS = new Set([
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowDown',
'Home',
'End',
]);
export interface ModalTriggerProps {
dialogClassName?: string;
triggerNode: ReactNode;
@@ -140,7 +149,16 @@ export const ModalTrigger = forwardRef(
draggableConfig={draggableConfig}
destroyOnHidden={destroyOnHidden}
>
{modalBody}
<div
style={{ display: 'contents' }}
onKeyDown={e => {
if (MENU_NAVIGATION_KEYS.has(e.key)) {
e.stopPropagation();
}
}}
>
{modalBody}
</div>
</Modal>
</>
);

View File

@@ -746,7 +746,10 @@ const AsyncSelect = forwardRef(
) => number
}
getPopupContainer={
getPopupContainer || (triggerNode => triggerNode.parentNode)
getPopupContainer ||
((triggerNode: HTMLElement) =>
(triggerNode?.closest('.ant-modal-content') as HTMLElement) ||
(triggerNode.parentNode as HTMLElement))
}
headerPosition={headerPosition}
labelInValue

View File

@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import { SelectOptionsType, SelectProps } from './types';
import { Select } from '.';
@@ -109,7 +110,7 @@ export const InteractiveSelect = ({
InteractiveSelect.args = {
mode: 'single',
placeholder: 'Select ...',
placeholder: t('Select ...'),
showSearch: true,
allowNewOptions: false,
allowClear: false,
@@ -414,7 +415,7 @@ AdvancedPlayground.args = {
oneLine: false,
optionsCount: options.length,
optionFilterProps: ['value', 'label', 'custom'],
placeholder: 'Select ...',
placeholder: t('Select ...'),
showSearch: true,
};

View File

@@ -811,7 +811,10 @@ const Select = forwardRef(
) => number
}
getPopupContainer={
getPopupContainer || (triggerNode => triggerNode.parentNode)
getPopupContainer ||
((triggerNode: HTMLElement) =>
(triggerNode?.closest('.ant-modal-content') as HTMLElement) ||
(triggerNode.parentNode as HTMLElement))
}
headerPosition={headerPosition}
labelInValue={labelInValue}

View File

@@ -211,6 +211,7 @@ export type {
GridState,
GridReadyEvent,
CellClickedEvent,
CellKeyDownEvent,
CellClassParams,
IMenuActionParams,
IHeaderParams,

View File

@@ -18,6 +18,7 @@
*/
import { AdhocFilter, DataMask } from '@superset-ui/core';
import { DatasourceType } from './Datasource';
export interface ColumnOption {
label: string;
@@ -38,6 +39,7 @@ export interface NativeFilterScope {
export interface NativeFilterTarget {
datasetId: number;
column: NativeFilterColumn;
datasourceType?: DatasourceType;
// maybe someday support this?
// show values from these columns in the filter options selector

View File

@@ -29,13 +29,11 @@ export enum FeatureFlag {
AlertReportWebhook = 'ALERT_REPORT_WEBHOOK',
AlertReportsFilter = 'ALERT_REPORTS_FILTER',
AllowFullCsvExport = 'ALLOW_FULL_CSV_EXPORT',
AvoidColorsCollision = 'AVOID_COLORS_COLLISION',
ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL',
ConfirmDashboardDiff = 'CONFIRM_DASHBOARD_DIFF',
CssTemplates = 'CSS_TEMPLATES',
DashboardVirtualization = 'DASHBOARD_VIRTUALIZATION',
DashboardVirtualizationDeferData = 'DASHBOARD_VIRTUALIZATION_DEFER_DATA',
DashboardRbac = 'DASHBOARD_RBAC',
DatapanelClosedByDefault = 'DATAPANEL_CLOSED_BY_DEFAULT',
DatasetFolders = 'DATASET_FOLDERS',
DateRangeTimeshiftsEnabled = 'DATE_RANGE_TIMESHIFTS_ENABLED',
@@ -48,6 +46,7 @@ export enum FeatureFlag {
EmbeddedSuperset = 'EMBEDDED_SUPERSET',
EnableAdvancedDataTypes = 'ENABLE_ADVANCED_DATA_TYPES',
EnableExtensions = 'ENABLE_EXTENSIONS',
EnableViewers = 'ENABLE_VIEWERS',
/** @deprecated */
EnableJavascriptControls = 'ENABLE_JAVASCRIPT_CONTROLS',
EnableTemplateProcessing = 'ENABLE_TEMPLATE_PROCESSING',

View File

@@ -95,6 +95,17 @@ describe('isProbablyHTML', () => {
expect(isProbablyHTML('price < $100')).toBe(false);
});
test('should return false for angle-bracketed data values (issue #34082)', () => {
// MySQL column values wrapped in angle brackets must not be treated as
// HTML, otherwise they get swallowed and appear truncated in SQL Lab
// and Table chart results
expect(isProbablyHTML('<Buddhist Blue Duck-No Giblets>')).toBe(false);
expect(isProbablyHTML('<Roasted Chicken-Whole>')).toBe(false);
expect(
isProbablyHTML('prefix <Buddhist Blue Duck-No Giblets> suffix'),
).toBe(false);
});
test('should return true for all known HTML tags', () => {
expect(isProbablyHTML('<section>Content</section>')).toBe(true);
expect(isProbablyHTML('<article>Content</article>')).toBe(true);
@@ -146,6 +157,12 @@ describe('safeHtmlSpan', () => {
const result = safeHtmlSpan(plainText);
expect(result).toEqual(plainText);
});
test('should return angle-bracketed data values untouched (issue #34082)', () => {
const dataValue = '<Buddhist Blue Duck-No Giblets>';
expect(safeHtmlSpan(dataValue)).toBe(dataValue);
expect(sanitizeHtmlIfNeeded(dataValue)).toBe(dataValue);
});
});
describe('removeHTMLTags', () => {

View File

@@ -28,6 +28,11 @@ import {
describe('CategoricalColorScale', () => {
beforeEach(() => {
window.featureFlags = {};
// the labels color map is a singleton: start every test from a clean
// explore-sourced state so dashboard-mode tests cannot leak into others
const labelsColorMap = getLabelsColorMap();
labelsColorMap.reset();
labelsColorMap.source = LabelsColorMapSource.Explore;
});
test('exists', () => {
@@ -159,9 +164,14 @@ describe('CategoricalColorScale', () => {
colorSet[color] = 1;
}
});
// collision avoidance redistributes recycled colors, so counts are
// not uniform; every color from the palette is still used and no
// color outside the palette appears
expect(Object.keys(colorSet)).toHaveLength(3);
const total = Object.values(colorSet).reduce((sum, n) => sum + n, 0);
expect(total).toBe(6);
['blue', 'red', 'green'].forEach(color => {
expect(colorSet[color]).toBe(2);
expect(colorSet[color]).toBeGreaterThanOrEqual(1);
});
});
test('get analogous colors when number of items exceed available colors', () => {
@@ -201,9 +211,6 @@ describe('CategoricalColorScale', () => {
expect(returnedColor).toBe(expectedColor);
});
test('reassigns colliding colors when no sliceId is provided', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
@@ -228,13 +235,10 @@ describe('CategoricalColorScale', () => {
expect(classicCarsColor).not.toBe('red');
} finally {
labelsColorMap.reset();
labelsColorMap.source = LabelsColorMapSource.Dashboard;
labelsColorMap.source = LabelsColorMapSource.Explore;
}
});
test('conditionally calls getNextAvailableColor', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
test('calls getNextAvailableColor for new labels with used colors', () => {
scale.labelsColorMapInstance.source = LabelsColorMapSource.Explore;
scale.getColor('testValue1');
@@ -250,19 +254,12 @@ describe('CategoricalColorScale', () => {
getNextAvailableColorSpy.mockClear();
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: false,
};
// existing labels reuse their assigned color without reassignment
scale.getColor('testValue3');
expect(getNextAvailableColorSpy).not.toHaveBeenCalled();
});
test('reassigns non-forced labels when a dashboard-synced label would duplicate their color', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
const dashScale = new CategoricalColorScale(['red', 'blue', 'green']);
const sliceId = 501;
const colorScheme = 'preset';
@@ -391,11 +388,12 @@ describe('CategoricalColorScale', () => {
test('returns the least used color among all', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
scale.getColor('fish'); // green
scale.getColor('puppy'); // blue
scale.getColor('teddy'); // red
// seed usage directly: getColor would itself redistribute collisions
scale.chartLabelsColorMap.set('cat', 'blue');
scale.chartLabelsColorMap.set('dog', 'red');
scale.chartLabelsColorMap.set('fish', 'green');
scale.chartLabelsColorMap.set('puppy', 'blue');
scale.chartLabelsColorMap.set('teddy', 'red');
// All colors used, so the function should return least used
expect(scale.getNextAvailableColor('darling', 'red')).toBe('green');
});
@@ -407,13 +405,14 @@ describe('CategoricalColorScale', () => {
'green',
'yellow',
]);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
scale.getColor('frog'); // green
scale.getColor('fish'); // yellow
scale.getColor('goat'); // blue
scale.getColor('horse'); // red
scale.getColor('pony'); // green
// seed usage directly: getColor would itself redistribute collisions
scale.chartLabelsColorMap.set('cat', 'blue');
scale.chartLabelsColorMap.set('dog', 'red');
scale.chartLabelsColorMap.set('frog', 'green');
scale.chartLabelsColorMap.set('fish', 'yellow');
scale.chartLabelsColorMap.set('goat', 'blue');
scale.chartLabelsColorMap.set('horse', 'red');
scale.chartLabelsColorMap.set('pony', 'green');
// Yellow is the least used color, so it should be returned.
expect(scale.getNextAvailableColor('pony', 'blue')).toBe('yellow');
@@ -537,9 +536,6 @@ describe('CategoricalColorScale', () => {
let labelsColorMap: ReturnType<typeof getLabelsColorMap>;
beforeEach(() => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
const sentinel = new CategoricalColorScale(['red', 'blue', 'green']);
labelsColorMap = sentinel.labelsColorMapInstance;
labelsColorMap.reset();
@@ -551,29 +547,6 @@ describe('CategoricalColorScale', () => {
labelsColorMap.reset();
});
test('reproduces the bug without the fix: Classic Cars and Trains would both be red', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: false,
};
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
expect(labelsColorMap.getColorMap().get('Trains')).toBe('red');
const chartBScale = new CategoricalColorScale(PALETTE);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
const classicCarsColor =
chartBScale.chartLabelsColorMap.get('Classic Cars');
const trainsColor = chartBScale.chartLabelsColorMap.get('Trains');
expect(trainsColor).toBe('red');
expect(classicCarsColor).toBe('red');
});
test('fix: Classic Cars is reassigned when Trains locks red from the dashboard', () => {
const PALETTE = ['red', 'blue', 'green'];
@@ -612,7 +585,6 @@ describe('CategoricalColorScale', () => {
test('fix: increments analogous color range for dashboard collisions when UseAnalogousColors is enabled', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
[FeatureFlag.UseAnalogousColors]: true,
};

View File

@@ -49,7 +49,7 @@ export interface VirtualDatasetCreatePayload {
schema: string | null;
table_name: string;
sql: string;
owners?: number[];
editors?: number[];
}
/**
@@ -65,7 +65,7 @@ export interface DatasetResult {
id: number;
database_name: string;
};
owners?: Array<{ id: number }>;
editors?: Array<{ id: number }>;
dataset_type?: 'physical' | 'virtual';
}
@@ -124,7 +124,7 @@ export async function createTestVirtualDataset(
schema: '',
table_name: name,
sql: "SELECT 1 as id, 'test' as name",
owners: [],
editors: [],
});
if (!response.ok()) {

View File

@@ -44,6 +44,29 @@ import { TIMEOUT } from '../../utils/constants';
const NONADMIN_USERNAME = process.env.PLAYWRIGHT_NONADMIN_USERNAME || 'gamma';
const NONADMIN_PASSWORD = process.env.PLAYWRIGHT_NONADMIN_PASSWORD || 'general';
const SUBJECT_TYPE_USER = 1;
type RelatedSubject = {
value: number;
text?: string;
extra?: {
type?: number;
secondary_label?: string;
};
};
function matchesUsername(subject: RelatedSubject, username: string) {
const normalizedUsername = username.toLowerCase();
const text = subject.text?.toLowerCase() ?? '';
const secondaryLabel = subject.extra?.secondary_label?.toLowerCase() ?? '';
return (
subject.extra?.type === SUBJECT_TYPE_USER &&
(text === normalizedUsername ||
text.includes(normalizedUsername) ||
secondaryLabel.includes(normalizedUsername))
);
}
// Clear storageState so the default page fixture starts unauthenticated.
// Admin API calls use an explicit admin context with saved auth instead.
@@ -87,17 +110,19 @@ test('non-admin user can view a themed dashboard without 403 or infinite spinner
dashboardId = dashBody.id;
expect(dashboardId).toBeTruthy();
// 3. Grant non-admin user access by adding them as a dashboard owner
// 3. Grant non-admin user access by adding them as a dashboard editor
const usersRes = await apiGet(
adminPage,
`api/v1/dashboard/related/owners?q=(filter:'${NONADMIN_USERNAME}')`,
`api/v1/dashboard/related/editors?q=(filter:'${NONADMIN_USERNAME}')`,
);
expect(usersRes.ok()).toBe(true);
const usersBody = await usersRes.json();
const gammaUserId = usersBody.result?.[0]?.value;
expect(gammaUserId).toBeTruthy();
const gammaSubjectId = (
usersBody.result as RelatedSubject[] | undefined
)?.find(subject => matchesUsername(subject, NONADMIN_USERNAME))?.value;
expect(gammaSubjectId).toBeTruthy();
const putRes = await apiPutDashboard(adminPage, dashboardId!, {
owners: [gammaUserId],
editors: [gammaSubjectId],
});
expect(putRes.ok()).toBe(true);

View File

@@ -40,6 +40,7 @@ import {
GridReadyEvent,
GridState,
CellClickedEvent,
CellKeyDownEvent,
SelectionChangedEvent,
} from '@superset-ui/core/components/ThemedAgGridReact';
import { t } from '@apache-superset/core/translation';
@@ -60,6 +61,7 @@ import reconcileColumnState from '../utils/reconcileColumnState';
import getColumnStateSignature from '../utils/getColumnStateSignature';
import { PAGE_SIZE_OPTIONS } from '../consts';
import { getCompleteFilterState } from '../utils/filterStateManager';
import { copyCellValueOnKeyDown } from '../utils/copyCellValue';
export interface AgGridState extends Partial<GridState> {
timestamp?: number;
@@ -236,6 +238,13 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
isSearchFocused.set(searchId, false);
}, [searchId]);
// Copy the focused cell's value on Ctrl/Cmd+C. Needed because cell text is
// no longer natively selectable (see enableCellTextSelection below) and the
// Enterprise clipboard module is not registered (#106389).
const handleCellKeyDown = useCallback((event: CellKeyDownEvent) => {
copyCellValueOnKeyDown(event);
}, []);
const onFilterTextBoxChanged = useCallback(
({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
if (serverPagination) {
@@ -515,13 +524,22 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
rowSelection="multiple"
animateRows
onCellClicked={handleCellClicked}
onCellKeyDown={handleCellKeyDown}
onSelectionChanged={handleSelectionChanged}
onFilterChanged={handleFilterChanged}
onStateUpdated={handleGridStateChange}
initialState={gridInitialState}
maintainColumnOrder
suppressAggFuncInHeader
enableCellTextSelection
// Clicking a cell should select (focus) the cell rather than select
// its text content (#106389). enableCellTextSelection forces browser
// text selection on click, which suppresses the cell-focus behavior.
// Because the Enterprise clipboard module isn't registered, native
// text selection was the only way to copy a value, so onCellKeyDown
// (above) restores Ctrl/Cmd+C copy for the focused cell. Full
// multi-cell range selection still requires AG Grid Enterprise, which
// is not available in the Community build used here.
enableCellTextSelection={false}
quickFilterText={serverPagination ? '' : quickFilterText}
suppressMovableColumns={!allowRearrangeColumns}
pagination={pagination}
@@ -529,6 +547,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
paginationPageSizeSelector={PAGE_SIZE_OPTIONS}
suppressDragLeaveHidesColumns
pinnedBottomRowData={showTotals ? [cleanedTotals] : undefined}
tooltipShowDelay={500}
localeText={{
// Pagination controls
next: t('Next'),

View File

@@ -448,10 +448,18 @@ export default function TableChart<D extends DataRecord = DataRecord>(
/>
);
const columnsForKeys = isUsingTimeComparison
? (filteredColumns as InputColumn[])
: (columns as InputColumn[]);
const descriptionsKey = columnsForKeys
.map(col => `${col.key}:${col.description ?? ''}`)
.join('|');
return (
<StyledChartContainer height={height}>
<AgGridDataTable
gridHeight={gridHeight}
key={descriptionsKey}
data={data || []}
colDefsFromProps={colDefs}
includeSearch={!!includeSearch}

View File

@@ -44,3 +44,11 @@ export const FILTER_CONDITION_BODY_INDEX = {
} as const;
export const ROW_NUMBER_COL_ID = '__row_number__';
// Symbol key used to attach a row's basic (increase/decrease) color formatter
// to the row data object so it travels with the row through AG Grid client-side
// sorting (#105973). A Symbol guarantees the key can never collide with a real
// dataset column (even one literally named "__basicColorFormatters__") and is
// automatically excluded from Object.keys, JSON serialization and spreads, so
// it never leaks into exports or cross-filters.
export const BASIC_COLOR_FORMATTERS_ROW_KEY = Symbol('basicColorFormatters');

View File

@@ -24,6 +24,7 @@ import {
import { CustomCellRendererProps } from '@superset-ui/core/components/ThemedAgGridReact';
import { BasicColorFormatterType, InputColumn, ValueRange } from '../types';
import { useIsDark } from '../utils/useTableTheme';
import getRowBasicColorFormatter from '../utils/getRowBasicColorFormatter';
const StyledTotalCell = styled.div`
${() => `
@@ -163,13 +164,13 @@ export const NumericCellRenderer = (
let arrow = '';
let arrowColor = '';
if (hasBasicColorFormatters && col?.metricName) {
arrow =
basicColorFormatters?.[node?.rowIndex as number]?.[col.metricName]
?.mainArrow;
arrowColor =
basicColorFormatters?.[node?.rowIndex as number]?.[
col.metricName
]?.arrowColor?.toLowerCase();
const rowFormatter = getRowBasicColorFormatter(
node,
node?.rowIndex,
basicColorFormatters,
)?.[col.metricName];
arrow = rowFormatter?.mainArrow ?? '';
arrowColor = rowFormatter?.arrowColor?.toLowerCase() ?? '';
}
const alignment =

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