Compare commits

...

178 Commits

Author SHA1 Message Date
dependabot[bot]
f3da949f61 chore(deps-dev): bump oxfmt from 0.70.0 to 0.71.0 in /superset-websocket (#45006)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 18:48:45 -07:00
dependabot[bot]
27d5c3655d chore(deps-dev): bump the typescript-eslint group in /superset-frontend with 2 updates (#45007)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 18:48:39 -07:00
dependabot[bot]
ee7b7fdc90 chore(deps-dev): bump oxlint from 1.85.0 to 1.86.0 in /docs (#45009)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 18:48:34 -07:00
dependabot[bot]
fc754d0e81 chore(deps-dev): bump oxlint from 1.85.0 to 1.86.0 in /superset-frontend (#45014)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 18:48:20 -07:00
dependabot[bot]
fc1eeb066a chore(deps-dev): bump wait-on from 9.1.0 to 9.4.0 in /superset-frontend (#45015)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 18:48:14 -07:00
dependabot[bot]
f88b2ef259 chore(deps): bump the security group across 1 directory with 6 updates (#45025)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 18:48:08 -07:00
dependabot[bot]
ac3fc943dd chore(deps): bump the security group across 1 directory with 9 updates (#45026)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 18:48:02 -07:00
Divyansh Yadav
715dc017df fix(explore): open SQL Lab in a new tab on Ctrl+click in View query modal (#44933) 2026-10-06 16:53:19 -07:00
Sepuri Sai Krishna
794b243c54 test(frontend): await remaining userEvent calls and lint for un-awaited ones (#44947) 2026-10-06 16:52:37 -07:00
Amogh Atreya
55e39fc5f7 fix(explore): avoid mutating ZoomConfigControl configs (#44957)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 16:47:16 -07:00
Amin Ghadersohi
5a2f4c8e80 feat(mcp): add structured dashboard text component management (#44560) 2026-10-06 16:48:40 -04:00
Amin Ghadersohi
17f4c215b3 feat(mcp): allow default values on filter_select native filters (#44985) 2026-10-06 16:48:25 -04:00
Trakshan Mishra
1473d48134 fix(query-context): match an adhoc granularity_sqla by its expression (#44773) 2026-10-06 13:37:29 -07:00
Abhinav
fcee985241 fix(pivot-table): respect per-metric formatters in result aggregation (#44815)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-10-06 13:22:21 -07:00
dependabot[bot]
7330ca4c37 chore(deps): bump source-map-js from 1.2.1 to 1.2.2 in /superset-embedded-sdk in the security group across 1 directory (#45024)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 13:19:53 -07:00
dependabot[bot]
3cfc745867 chore(deps): bump dawidd6/action-download-artifact from 26 to 27 (#45011)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 13:19:34 -07:00
dependabot[bot]
3413a50af0 chore(deps): bump proxy-addr from 2.0.7 to 2.0.8 in /superset-websocket/utils/client-ws-app in the security group across 1 directory (#45027)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 13:19:13 -07:00
Mike Bridge
d6c871a57f fix(semantic-layers): remove child view permissions when a layer is deleted (#44905)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 16:15:37 -04:00
Mike Bridge
000297a6f0 fix(semantic-layers): offer valid table ordering choices (#44806)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-10-06 16:15:26 -04:00
Mike Bridge
854d3d59e4 fix(table): omit dormant grains from semantic aggregate requests (#44455) 2026-10-06 16:15:13 -04:00
drivaspreset
a5ffac58f4 feat: add global async query playwright tests (#43004)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-10-06 12:03:54 -07:00
dependabot[bot]
457007b2f8 chore(deps-dev): bump source-map-js from 1.2.1 to 1.2.2 in /superset-websocket in the security group across 1 directory (#45028)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 11:08:46 -07:00
dependabot[bot]
9d348facce chore(deps): bump @swc/core from 1.16.2 to 1.16.12 in /docs (#45008)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 11:03:56 -07:00
dependabot[bot]
9e18d415d9 chore(deps-dev): bump @swc/core from 1.16.2 to 1.16.12 in /superset-frontend in the swc group (#45012)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 11:01:19 -07:00
dependabot[bot]
f9e9ece86f chore(deps): bump the rjsf group in /superset-frontend with 3 updates (#45004)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-10-06 10:51:14 -07:00
Evan Rusackas
7b3e3ad6f7 fix(dashboard,explore): wire addWarningToast into download callers (#44154)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 10:50:25 -07:00
Evan Rusackas
4578ab31ad refactor(list-view): eliminate any usage in ListView.tsx and TableCollection (#44208)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 10:49:59 -07:00
Evan Rusackas
518bfbc511 fix(reports): propagate force flag to dashboard-tab permalink report URLs (#44775)
Co-authored-by: Ferjani Nasraoui <ferjani.nasraoui@europace.de>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 10:49:39 -07:00
Evan Rusackas
bc1abf498d feat(bignumber): add an alignment control (#44554)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 10:49:18 -07:00
Evan Rusackas
7b3ce2cf36 fix(themes): overwrite-import guard, missing index, dedupe extra_editors (follow-up to #42404) (#44362)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-06 10:49:07 -07:00
Evan Rusackas
6aebcd2452 fix(dashboards): close CSS validation gaps in dashboard import and edits (#43666)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 10:47:17 -07:00
Israel Demetrios Diacov
e0ff107487 chore(i18n): update pt/pt_BR translations and rebuild translation index (#43022)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Israel Demetrios Diacov <israelddiacov@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 10:46:30 -07:00
SBIN2010
7b463ddee0 feat(ag-grid-table): expand JSON values in table cells (#44907)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-10-06 10:14:03 -07:00
dependabot[bot]
9a345ad011 chore(deps-dev): bump oxlint from 1.85.0 to 1.86.0 in /superset-websocket (#45005)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 10:10:57 -07:00
Mike Bridge
c30ae9fd12 fix(embedded): refuse guest row-level security on semantic views (#44987)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-10-06 10:07:46 -07:00
Đỗ Trọng Hải
71ee07e2e2 feat(ci): conditionally run CodeQL analysis workflows only when there are detected JS/Python file changes (#44699) 2026-10-06 23:36:01 +07:00
Rafael Benitez
31e3c9a194 fix(ag-grid-table): refresh totals when summary aggregation changes (#44612) 2026-10-06 11:30:19 -03:00
Amin Ghadersohi
8ff884059f fix(logging): stop logging tracebacks for client-side HTTP errors (#44666) 2026-10-06 08:05:46 -04:00
Amin Ghadersohi
05a3d4b880 feat(mcp): return the Big Number headline from chart and dashboard data (#44948) 2026-10-06 07:24:56 -04:00
Amin Ghadersohi
13493a3e47 fix(mcp): allow bounding dashboard dataset columns (#44951) 2026-10-06 07:24:36 -04:00
Mike Bridge
ce39d221aa fix(charts): return 404 when chart export hits an inaccessible dataset (#44900)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 20:40:36 -04:00
Amin Ghadersohi
2fdf756cf1 fix(mcp): avoid duplicate SQL execution results (#44949) 2026-10-05 20:36:41 -04:00
Elizabeth Thompson
0c6373cd9f fix(models): silence pandas silent-downcasting FutureWarning in normalize_df (#44897)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 15:01:47 -07:00
Amin Ghadersohi
3e50c2c128 feat(mcp): support filter_range and filter_timegrain filters (#44893) 2026-10-05 16:10:51 -04:00
Beto Dealmeida
895bc20360 fix: full CSV download in AgGrid (#41696)
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-05 12:34:16 -07:00
Đỗ Trọng Hải
13861568aa chore(build): remove unused polyline Python dep (#44961) 2026-10-06 00:18:38 +07:00
dependabot[bot]
0616ba68af chore(deps-dev): bump the swc group in /superset-frontend with 2 updates (#44975)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 10:17:45 -07:00
Amin Ghadersohi
48ac1bf8c6 fix(mcp): return actionable authorized column suggestions (#44603) 2026-10-05 13:15:31 -04:00
Minwook Shin
09015bb4bf fix(explore): preserve pending column configuration edits (#44931) 2026-10-05 10:10:55 -07:00
Sam Firke
7fecceb690 chore: remove unused INCLUDE_FIREFOX build arg and dead screenshot config (#44245) 2026-10-05 13:10:18 -04:00
dependabot[bot]
31d2d06bba chore(deps): bump ws from 8.21.3 to 8.22.0 in /superset-websocket (#44969)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 09:53:45 -07:00
dependabot[bot]
83ea1d9e6f chore(deps-dev): bump vitest from 5.0.1 to 5.0.2 in /superset-websocket (#44970)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 09:53:38 -07:00
dependabot[bot]
303a68b8e2 chore(deps-dev): bump @types/node from 26.6.2 to 26.6.3 in /superset-websocket (#44971)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 09:53:33 -07:00
dependabot[bot]
c00f6a7aab chore(deps-dev): bump @types/node from 26.6.2 to 26.6.3 in /superset-embedded-sdk (#44972)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 09:53:27 -07:00
dependabot[bot]
d62fc92273 chore(deps-dev): bump vitest from 5.0.1 to 5.0.2 in /superset-embedded-sdk (#44974)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 09:53:20 -07:00
dependabot[bot]
343dcb0935 chore(deps): bump chalk from 6.0.0 to 6.0.1 in /superset-frontend (#44976)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 09:53:15 -07:00
dependabot[bot]
fee8e1b9e5 chore(deps-dev): bump webpack-sources from 3.5.3 to 3.6.0 in /superset-frontend (#44978)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 09:53:10 -07:00
dependabot[bot]
bc69daa232 chore(deps-dev): bump @types/node from 26.6.2 to 26.6.3 in /superset-frontend (#44979)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 09:53:04 -07:00
dependabot[bot]
7a62b3a653 chore(deps-dev): bump postcss-styled-syntax from 0.7.2 to 0.7.3 in /superset-frontend (#44980)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 09:52:58 -07:00
dependabot[bot]
3ac856ceb1 chore(deps): bump chromaui/action from 18.7.3 to 18.10.1 (#44973) 2026-10-05 23:26:33 +07:00
dependabot[bot]
46b9ba9587 chore(deps): bump dawidd6/action-download-artifact from 25 to 26 (#44977) 2026-10-05 23:25:56 +07:00
Daniel Alyoshin
7a2913e2db fix(sqlite): write midnight as a bare date for DATE columns in time filters (#44805)
Co-authored-by: Amin Ghadersohi <5183956+aminghadersohi@users.noreply.github.com>
2026-10-04 19:51:11 -04:00
Evan Rusackas
b5b8a9f3bd fix(dataset-editor): preserve edits across sort and sync external SQL changes (#44858)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-04 16:30:14 -07:00
Evan Rusackas
68f19947a8 docs(mcp): document semantic-layer MCP tools (#44130)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 21:57:29 -07:00
Joe Li
ccae778498 fix(mypy): ignore false-positive union-attr on Slice.uuid.in_() (#44944)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-04 11:48:28 +07:00
Amin Ghadersohi
cdafffeacf fix(deps): restore dompurify 3.4.16 in frontend lockfile (#44960) 2026-10-04 09:58:29 +07:00
Evan Rusackas
5d158e7e79 test(cratedb): cover epoch-ms timestamp decoding against a real instance (#44904)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 14:01:51 -07:00
Evan Rusackas
e3bdb3894c test(oracle): cover cancel-query against a real running statement (#44908)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 13:59:53 -07:00
Evan Rusackas
e72d63b20d test(mysql): cover require_mysql_tls fail-closed and verified-TLS paths (#44910)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 13:57:44 -07:00
Evan Rusackas
acdd390b35 fix(frontend): finish migrating off direct antd imports, enforce it in custom rules (#44927)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 13:55:58 -07:00
Evan Rusackas
3fccf29946 test(opensearch): cover pagination and Content-Type regression against a real instance (#44924)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 11:49:00 -07:00
Jay Masiwal
f2ff458acd feat: Add GUI for label_colors in Dashboard Properties Modal (#39434)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Jay Masiwal <jaymasiwal@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 20:11:44 +03:00
Amin Ghadersohi
549508b340 fix(mcp): preserve calling constraints in compact tool discovery (#44656)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 05:35:46 -04:00
dependabot[bot]
98be2d17da chore(deps-dev): bump clickhouse-connect from 1.8.0 to 1.9.0 (#44934)
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-10-03 02:19:53 -07:00
dependabot[bot]
291d098917 chore(deps): bump sqlglot from 30.18.0 to 30.19.0 (#44935)
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-10-03 02:19:47 -07:00
dependabot[bot]
8ba2667967 chore(deps): bump holidays from 0.104 to 0.105 (#44936)
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-10-03 02:19:41 -07:00
dependabot[bot]
dbd3e28aa3 chore(deps-dev): update databricks-sql-connector requirement from <4.6.0,>=4.5.0 to >=4.6.0,<4.7.0 (#44937)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 02:19:36 -07:00
dependabot[bot]
3960407334 chore(deps): bump cachetools from 7.1.8 to 7.2.0 (#44939)
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-10-03 02:19:30 -07:00
dependabot[bot]
a65fe06c51 chore(deps-dev): update google-cloud-storage requirement from >=3.14.1 to >=3.15.0 (#44940)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 02:19:24 -07:00
dependabot[bot]
3a9becd5cb chore(deps): bump markdown from 3.10.3 to 3.11 (#44941)
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-10-03 02:19:19 -07:00
Amin Ghadersohi
afa98f3391 perf(deletion-retention): one window pass for repeat predicate (#44349) 2026-10-03 04:58:35 -04:00
Evan Rusackas
f1ae8909d8 fix(import): remove duplicate config redefinition in load_configs (#44932)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 17:53:57 +10:00
Sepuri Sai Krishna
ea79c63e87 fix(post-processing): stop treating gaps as zero for cumprod, cummin and cummax (#44828) 2026-10-02 22:22:50 -07:00
Amin Ghadersohi
81466987cb fix(mcp): use create_proxy in simple_proxy for fastmcp 4 compatibility (#44787) 2026-10-02 22:21:39 -07:00
Endi Monan
6966c1f6cc fix(cartodiagram): share Pie colors across locations in Explore (#44794) 2026-10-02 22:17:49 -07:00
Nguyen Dang Trung Tien
10356c30ed fix(mcp): stop partial-update tools from advertising null defaults (#44573)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 22:16:12 -07:00
hahaok
d668928394 fix(sql-lab): use function valueGetter for GridTable row numbers (#41574)
Co-authored-by: chenshaobo <chenshaobo@yjsafe.cn>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-10-02 22:14:33 -07:00
Piyush Raj
87bb752814 fix(select): remove Space wrapper from optionRender to fix option label truncation (#44357) 2026-10-02 22:05:55 -07:00
Joe Li
743a27f7ed fix(chart): sort Heatmap Y-axis by default when unset (#44588)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-10-02 22:03:57 -07:00
Dennis Khylkouski
62ab4cae23 fix(sql-lab): avoid duplicate generated result column names (#44189) 2026-10-02 22:03:18 -07:00
Sepuri Sai Krishna
2220f53d9d fix(chart): accept quarter and day in end-of time ranges (#43204) 2026-10-02 21:46:07 -07:00
Đỗ Trọng Hải
ba453d3cb4 fix(sec): sanitize HTML text before shown as impact item's label (#43825) 2026-10-03 11:44:16 +07:00
Evan Rusackas
646de3a46b test(clickhouse): cover GROUP BY ALL against a real instance (#40482) (#44879)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-02 19:43:21 -07:00
Evan Rusackas
7260916b90 test(dashboard): cover "View as table" end-to-end for a view-as-table-only role (#44881)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-02 19:42:49 -07:00
Amin Ghadersohi
8d026b4a56 fix(mcp): skip dashboard live updates when websockets are disabled or realtime access is missing (#44796) 2026-10-03 11:48:02 +10:00
Evan Rusackas
11bdbc91f3 feat(ci): add Chromatic visual regression testing for Storybook (#44103)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-02 18:26:28 -07:00
Evan Rusackas
15bde2a301 fix(native-filters): keep cascade dependency gate in sync with live filter type (#44366)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-02 18:26:23 -07:00
Evan Rusackas
52d8b405c9 fix(echarts): recognize Date and ISO-string temporal x-axis values in getXAxisDomain (#44818)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-02 18:26:15 -07:00
Amin Ghadersohi
3fa06db85b fix(mcp): include feature_availability in instance://metadata resource (#44891) 2026-10-02 19:50:28 -04:00
Sam Firke
396030d87e docs: remove stale Selenium references after Playwright-only switch (#44243) 2026-10-02 18:09:31 -04:00
Elizabeth Thompson
f9d67e1a11 fix: downgrade deprecated query_object field warnings to info (#43520) 2026-10-02 15:01:50 -07:00
Elizabeth Thompson
ef2c05c090 fix(security): guard is_guest_user against NoAuthorizationError on unauthenticated error paths (#43826)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 15:01:46 -07:00
Elizabeth Thompson
47ba8fc801 fix(api): return 401 not 500 for auth errors in CurrentUserRestApi (SC-120417) (#44213) 2026-10-02 15:01:41 -07:00
Elizabeth Thompson
ece8b8bd7b fix(import): avoid UnboundLocalError when load_yaml fails during load_configs (SC-121288) (#44390)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 15:01:37 -07:00
Elizabeth Thompson
3f7d01aaaf fix(reports): catch TypeError when validating non-string extra.dashboard.anchor (#44404)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 15:01:31 -07:00
Elizabeth Thompson
7fea33761b fix(chart): wrap raw pandas TypeError/DataError from post-processing as QueryObjectValidationError (#44463) 2026-10-02 15:01:26 -07:00
Sepuri Sai Krishna
2872d16ee6 test(frontend): await the userEvent calls that needed restructuring (#44799) 2026-10-02 14:56:47 -07:00
Joe Li
bc3698b5d6 test(explore): port remaining deleted Cypress explore specs to RTL (#44838)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:25:34 -07:00
Joe Li
240a4267f8 test(explore): add datasourcesReducer unit tests (#44840)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 10:33:56 -07:00
Joe Li
f6155b9e9e fix(dashboard): show the configured refresh warning alongside the limit error (#44836)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 10:28:43 -07:00
Joe Li
e32493c9d9 test(explore): add saveModalReducer unit tests (#44839)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 10:28:34 -07:00
Joe Li
03b61cc2d8 test(explore): add fetchTopNValues unit tests (#44841)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 10:25:35 -07:00
Joe Li
8849542bda test(explore): cover viz switch and control dependency logic (#44842)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:25:24 -07:00
Joe Li
8d25777b99 test(plugin-chart-table): cover server-side sort, query mode controls, and sort ordering (#44845)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:25:11 -07:00
Joe Li
b0ac9ef61f test(explore): cover color scheme selection, BigNumber subheader/trendline, and WorldMap bubbles (#44846)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:24:47 -07:00
Luiz Otavio
ef64bacbaa feat(chart): cross-filter by x-axis label on charts with dimensions (#44869)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:16:28 -07:00
Amin Ghadersohi
e606cadff7 chore(mcp): fix malformed tool and prompt docstrings (#44572)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 12:49:19 -04:00
Krishna kumar singh
c1bb129c21 fix(change-detector): classify changed files by language, not directory (#44895) 2026-10-02 23:41:05 +07:00
Joe Li
e048389872 test(explore): cover time range frames, comparison labels, and metric popover state (#44847)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:10:45 -07:00
Alasdair Brown
0fdfd6660e docs(databases): add ClickHouse Managed Postgres (#44870)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-10-02 22:25:27 +07:00
Joe Li
10d06859b8 fix(ci): floor pyfakefs at 5.7.4 to fix Python 3.13 pytest-cov crash (#44853)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 21:08:54 +07:00
Đỗ Trọng Hải
005edd7423 chore(build): de-vendor helm/chart-testing-action GHA (#44722)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-10-02 20:59:36 +07:00
dependabot[bot]
7bb2964941 chore(deps): bump dawidd6/action-download-artifact from 24 to 25 (#44884)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 20:58:55 +07:00
Younes Beriane
1b0a37cf22 fix(users): stop update_me setting self-referential changed_by_fk (#44866)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 15:10:55 +02:00
Mallikarjuna Reddy Nimmakayala
ba3500e0e9 feat(export/import): add annotation layer export/import support for charts and dashboards (#43232)
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com>
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-10-02 15:05:18 +02:00
Evan Rusackas
25956d6c44 chore(superset-ui-chart-controls): forward-compat fixes for TypeScript 6.0 (#44877)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-02 03:53:24 -07:00
dependabot[bot]
27c9213cca chore(deps-dev): bump oxlint-tsgolint from 7.0.2002 to 7.0.2003 in /superset-websocket (#44882)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 00:57:45 -07:00
dependabot[bot]
95faf3a6a0 chore(deps-dev): bump oxlint-tsgolint from 7.0.2002 to 7.0.2003 in /docs (#44883)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 00:57:40 -07:00
dependabot[bot]
c43770e12d chore(deps-dev): bump webpack-sources from 3.5.1 to 3.5.3 in /superset-frontend (#44885)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 00:57:35 -07:00
dependabot[bot]
dfccc2e1e9 chore(deps-dev): bump minimizer-webpack-plugin from 5.11.0 to 5.12.0 in /superset-frontend (#44886)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 00:57:30 -07:00
dependabot[bot]
6559b483f2 chore(deps): bump maplibre-gl from 6.8.0 to 6.11.2 in /superset-frontend (#44887)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 00:57:24 -07:00
dependabot[bot]
4a19948659 chore(deps): bump dom-to-image-more from 3.10.2 to 3.11.0 in /superset-frontend (#44888)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 00:57:18 -07:00
dependabot[bot]
338e640df9 chore(deps): bump dompurify from 3.4.15 to 3.4.16 in /superset-frontend (#44889)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 00:57:12 -07:00
dependabot[bot]
edc91b86e5 chore(deps-dev): bump baseline-browser-mapping from 2.11.25 to 2.11.26 in /superset-frontend (#44890)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 00:57:07 -07:00
Amin Ghadersohi
9c7c96fe05 fix(logging): register LogRestApi only once (#44732) 2026-10-02 02:28:52 -04:00
Mike Bridge
b94f88ffbb fix(semantic-layer): require explicit member identity reselection (#44370) 2026-10-02 15:10:18 +10:00
Mike Bridge
6d97cde59a fix(semantic-layers): export/import semantic-view charts by typed reference (#44396) 2026-10-02 15:09:51 +10:00
Mike Bridge
21b937b59d fix(retention): skip models without purge policies before scanning (#44874) 2026-10-02 15:09:06 +10:00
Amin Ghadersohi
342a8f1e4e fix(mcp): report a chart's live dataset id and name (#44681) 2026-10-01 23:19:32 -04:00
Evan Rusackas
db56168a86 ci: require babel-extract to pass before merging master (#44543)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 20:12:46 -07:00
Amin Ghadersohi
f7badeb095 fix(mcp): execute_sql request limit caps, never raises, an explicit SQL LIMIT (#44604)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:09:17 -04:00
Archita-kale
e770202034 fix(postprocessing): preserve NULL index values through pivot() (#43547) (#43693)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 20:03:58 -07:00
Amin Ghadersohi
6a832cb210 fix(mcp): enforce dashboard filter scope on dataset, SQL and chart tool calls (#44800) 2026-10-01 21:31:32 -04:00
dependabot[bot]
3f6d4cd689 chore(deps-dev): update google-cloud-storage requirement from >=1.37 to >=3.14.1 (#44693)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 18:16:37 -07:00
Bexultan
abdf2e6c70 fix(home): redirect users without an ID before rendering (#44456) 2026-10-01 17:49:38 -07:00
dependabot[bot]
c82f97b69d chore(deps): bump deck.gl and luma.gl from 9.2.5 to 9.4.0 in /superset-frontend (#42608)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 17:36:33 -07:00
hadi mobarra
217e4c0223 fix(frontend): use html2canvas for chart image export on Safari (#44529) 2026-10-01 16:52:15 -07:00
Greg Neighbors
446853004a fix(mcp): keep a bubble chart's colors and row limit across updates (#44618)
Co-authored-by: Greg Neighbors <gregneighbors@Gregs-Air-2.lan>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:51:47 -07:00
Viktor Högberg
2c72d4d4fe fix(mcp): use DEFAULT_PAGE_SIZE constant in list_charts test (#44786) 2026-10-01 16:51:04 -07:00
mattmc3
4ae8fe7122 fix: size 'Drill to detail' table header correctly (#44807) 2026-10-01 16:50:28 -07:00
Amin Ghadersohi
33292f249d feat(mcp): support tab-scoped dashboard layouts (#44797) 2026-10-01 18:42:33 -04:00
Amin Ghadersohi
0a0a1974c5 test(playwright): select existing dashboards without creating duplicates (#44856) 2026-10-01 18:32:19 -04:00
Amin Ghadersohi
a12d1b8e12 fix(mcp): enforce tool deadlines without blocking the server (#44581)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 18:30:25 -04:00
SBIN2010
e5ec22ab3e feat(table): add multi-level column header groups (#43938)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 01:05:04 +03:00
Amin Ghadersohi
ee39954adb fix(oauth2): refresh a token rejected when a connection opens (#44765) 2026-10-01 17:46:30 -04:00
Joe Li
8ab7a85a3d chore(e2e): remove Cypress infrastructure (#44829)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:32:51 -07:00
dependabot[bot]
74d885233d chore(deps): bump github/codeql-action/init from 4.38.1 to 4.38.2 (#44860)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-10-01 10:39:30 -07:00
Evan Rusackas
b98e19f6eb chore: drop cypress-matrix-required from required status checks (#44854)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 06:39:09 -07:00
Evan Rusackas
8b643519ca chore: add sadpandajoe as a codeowner for .asf.yaml (#44855)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 06:38:40 -07:00
dependabot[bot]
6418473be1 chore(deps): bump github/codeql-action/upload-sarif from 4.38.1 to 4.38.2 (#44859)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 01:09:36 -07:00
dependabot[bot]
4f9c22db6d chore(deps): bump github/codeql-action/analyze from 4.38.1 to 4.38.2 (#44861)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 01:09:24 -07:00
dependabot[bot]
3109cfaf6d chore(deps): bump @googleapis/sheets from 18.0.0 to 18.0.1 in /superset-frontend (#44862)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 01:09:17 -07:00
Amin Ghadersohi
4911dbe00b fix(doris): offer the connection form by matching the installed driver (#44736) 2026-10-01 02:28:53 -04:00
Vikash Kumar
4cdd680f39 fix: increase dataset edit modal size (#38215) (#39257)
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-09-30 21:33:05 -07:00
Evan Rusackas
d2fb52ac83 fix(auth): remove the legacy FAB password reset views and move password resets into the SPA (#44626)
Co-authored-by: jayvenn21 <jvennamreddy@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 20:23:16 -07:00
Amin Ghadersohi
924343e3ee fix(gantt): prevent y-axis category labels from being clipped (#44321) 2026-09-30 19:58:52 -07:00
47th
e91ce19a60 fix(auth): drain flash messages on the login page (#44605) 2026-09-30 19:52:26 -07:00
chadek
90fc473583 test(embedded-sdk): cross-document test rig for the navigation fix (#44608)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-30 19:52:06 -07:00
Amin Ghadersohi
bbf00956d4 fix(mcp): prioritize exact tool names in BM25 search (#44682) 2026-09-30 19:51:09 -07:00
Amin Ghadersohi
855bb4e21c fix(gsheets): align service-account validation and serialize upload dates (#44695) 2026-09-30 19:50:52 -07:00
Amin Ghadersohi
5cf8213354 fix(databricks): keep the user's OAuth2 token and extra connect_args; re-auth on HTTP 401 (#44705) 2026-09-30 19:50:32 -07:00
Amin Ghadersohi
8baa9c1f2e fix(db2): accept sqlglot's parse_mod in the DB2 term parser (#44708) 2026-09-30 19:50:17 -07:00
Amin Ghadersohi
dc0e306a24 fix(elasticsearch): classify byte, short, half_float, scaled_float and unsigned_long columns (#44713) 2026-09-30 19:49:48 -07:00
Amin Ghadersohi
d2101c8dba fix(exasol): classify common server errors (#44721) 2026-09-30 19:49:26 -07:00
Amin Ghadersohi
fd524f4b21 fix(sqllab): preserve exact decimals in results and exports (#44739) 2026-09-30 19:48:43 -07:00
Shaitan
f02f53df23 fix(sql): reject client-side file-transfer statements in query execution (#44496)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-09-30 19:37:23 -07:00
Gaston Laterza
d80fcfc02a fix(i18n): make babel_update.sh .pot normalization actually run (#44395) 2026-10-01 08:04:46 +07:00
Gaurav Dubey
bbf0fa5f42 fix(core): stop discarding API errors that quote an HTML tag (#42489)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-30 17:03:04 -07:00
691 changed files with 80397 additions and 29994 deletions

View File

@@ -77,7 +77,7 @@ github:
# combination here.
contexts:
- lint-check
- cypress-matrix-required
- babel-extract
- dependency-review
- enforce-single-migration-head
- frontend-build

View File

@@ -20,8 +20,8 @@ Apache Superset is a data visualization platform with Flask/Python backend and R
### Testing Strategy Migration
- **Prefer unit tests** over integration tests
- **Prefer integration tests** over Cypress end-to-end tests
- **Cypress is last resort** - Actively moving away from Cypress
- **Prefer integration tests** over end-to-end tests
- **Use Playwright for E2E tests** - Cypress has been removed
- **Use Jest + React Testing Library** for component testing
### Backend Type Safety

View File

@@ -39,7 +39,6 @@
tests/
docs/
install/
superset-frontend/cypress-base/
superset-frontend/coverage/
superset-frontend/.temp_cache/
superset/static/assets/

View File

@@ -32,7 +32,6 @@ _p=80; while ! _is_free $_p; do ((_p++)); done; export NGINX_PORT=$_p
_p=8088; while ! _is_free $_p; do ((_p++)); done; export SUPERSET_PORT=$_p
_p=9000; while ! _is_free $_p; do ((_p++)); done; export NODE_PORT=$_p
_p=8080; while ! _is_free $_p || [ $_p -eq $NGINX_PORT ]; do ((_p++)); done; export WEBSOCKET_PORT=$_p
_p=8081; while ! _is_free $_p || [ $_p -eq $WEBSOCKET_PORT ]; do ((_p++)); done; export CYPRESS_PORT=$_p
_p=5432; while ! _is_free $_p; do ((_p++)); done; export DATABASE_PORT=$_p
_p=6379; while ! _is_free $_p; do ((_p++)); done; export REDIS_PORT=$_p

2
.github/CODEOWNERS vendored
View File

@@ -9,7 +9,7 @@
# Notify PMC members of changes to required GitHub Actions
/.asf.yaml @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @dpgaspar @sha174n @Antonio-RiveroMartnez @msyavuz
/.asf.yaml @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @dpgaspar @sha174n @sadpandajoe @Antonio-RiveroMartnez @msyavuz
# Maps are fragile and political. GeoJson edits MUST be made in the Jupyter notebook or they'll be overwritten.

View File

@@ -20,6 +20,11 @@ outputs:
superset-extensions-cli:
description: Whether superset-extensions-cli package-related files were changed
value: ${{ steps.change-detector.outputs.superset-extensions-cli }}
languages:
description: >-
JSON array of CodeQL languages touched, classified by file extension
regardless of directory (e.g. ["javascript", "python"])
value: ${{ steps.change-detector.outputs.languages }}
runs:
using: composite
steps:

2
.github/labeler.yml vendored
View File

@@ -54,8 +54,6 @@
- 'superset-embedded-sdk/package-lock.json'
- 'superset-websocket/package.json'
- 'superset-websocket/package-lock.json'
- 'superset-frontend/cypress-base/package.json'
- 'superset-frontend/cypress-base/package-lock.json'
- 'superset-frontend/packages/**/package.json'
- 'superset-frontend/plugins/**/package.json'

View File

@@ -161,108 +161,6 @@ celery-worker() {
say "::endgroup::"
}
cypress-install() {
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
cache-restore cypress
say "::group::Install Cypress"
npm ci
say "::endgroup::"
cache-save cypress
}
cypress-run-all() {
local USE_DASHBOARD=$1
local APP_ROOT=$2
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
# Start the Superset backend via gunicorn (not `flask run`). The Flask
# development server is single-threaded and has no crash-recovery, so
# heavy tests (dashboard import/export, SQL Lab) can knock it offline
# for the rest of the run — surfacing as `ECONNREFUSED` / `socket hang up`
# / `Missing CSRF token` cascades. Gunicorn gives us multiple workers,
# a request timeout, and worker-recycling under load.
local serverlog="${HOME}/superset-cypress.log"
local port=8081
CYPRESS_BASE_URL="http://localhost:${port}"
if [ -n "$APP_ROOT" ]; then
export SUPERSET_APP_ROOT=$APP_ROOT
CYPRESS_BASE_URL=${CYPRESS_BASE_URL}${APP_ROOT}
fi
export CYPRESS_BASE_URL
# Mirrors the args in docker/entrypoints/run-server.sh (1 worker × 20
# gthread threads) to keep parity with production. Multi-worker
# configurations expose timing-sensitive races in the SQL Lab → Explore
# navigation flow under E2E. We diverge from the entrypoint on:
# --timeout 120: heavy dashboard import/export specs exceed the 60s
# default
# superset.app:create_app(): explicit factory so we don't depend on
# FLASK_APP being exported
#
# No --max-requests, matching the entrypoint's default of 0 (recycling
# off). With a single worker a recycle takes the whole backend offline for
# the graceful-timeout drain — browser keep-alive connections hold it open
# for the full 30s — plus ~5s of app boot. A run issues ~3800 requests in
# ~8 minutes, so recycling every 500 produced seven ~35s outages per run
# and flaked whichever specs happened to navigate into one. Lowering
# --graceful-timeout is not enough: a dashboard load plus chart render
# needs 6-10s, which still lands inside the window.
nohup gunicorn \
--bind "127.0.0.1:$port" \
--workers 1 \
--worker-class gthread \
--threads 20 \
--timeout 120 \
--access-logfile - \
--error-logfile - \
"superset.app:create_app()" \
>"$serverlog" 2>&1 </dev/null &
local serverPid=$!
# Ensure the backend is cleaned up and its log is emitted even when the
# test runner fails under `set -e`.
trap '
echo "::group::gunicorn log for Cypress run"
cat "'"$serverlog"'" || true
echo "::endgroup::"
kill '"$serverPid"' 2>/dev/null || true
' EXIT
# Wait for the backend to be ready before launching Cypress; otherwise
# the first spec can race the server bind and see connection errors.
local timeout=60
say "Waiting for gunicorn server to start on port $port..."
while [ $timeout -gt 0 ]; do
if curl -f "http://localhost:${port}${APP_ROOT}/health" >/dev/null 2>&1; then
say "gunicorn server is ready"
break
fi
sleep 1
timeout=$((timeout - 1))
done
if [ $timeout -eq 0 ]; then
echo "::error::gunicorn server failed to start within 60 seconds"
echo "::group::Server startup log"
cat "$serverlog"
echo "::endgroup::"
return 1
fi
USE_DASHBOARD_FLAG=''
if [ "$USE_DASHBOARD" = "true" ]; then
USE_DASHBOARD_FLAG='--use-dashboard'
fi
# UNCOMMENT the next few commands to monitor memory usage
# monitor_memory & # Start memory monitoring in the background
# memoryMonitorPid=$!
python ../../scripts/cypress_run.py --retries 5 $USE_DASHBOARD_FLAG
# kill $memoryMonitorPid
}
playwright-install() {
cd "$GITHUB_WORKSPACE/superset-frontend"
@@ -276,11 +174,20 @@ playwright-install() {
playwright-run() {
local APP_ROOT=$1
local TEST_PATH=$2
shift || true
# Remaining arguments are test paths, relative to playwright/tests. Passing
# more than one lets a caller run a suite that spans directories. With none,
# selection comes from PLAYWRIGHT_EXTRA_ARGS instead -- that is how
# playwright-run-gaq picks its suite, via --project.
local TEST_PATHS=("$@")
local TEST_PATH=${TEST_PATHS[0]:-}
# Start the Superset backend via gunicorn from the project root.
# See cypress-run-all() above for the rationale — the Flask dev server
# cannot survive the dashboard import/export tests under load.
# Start the Superset backend via gunicorn (not `flask run`). The Flask
# development server is single-threaded and has no crash-recovery, so
# heavy tests (dashboard import/export, SQL Lab) can knock it offline
# for the rest of the run — surfacing as `ECONNREFUSED` / `socket hang up`
# / `Missing CSRF token` cascades. Gunicorn gives us a request timeout
# and a multi-threaded worker.
cd "$GITHUB_WORKSPACE"
local serverlog="${HOME}/superset-playwright.log"
local port=8081
@@ -296,9 +203,22 @@ playwright-run() {
fi
export PLAYWRIGHT_BASE_URL
# See cypress-run-all() above for the args rationale (1 worker × 20
# gthread threads matching docker/entrypoints/run-server.sh, a 120s
# timeout for heavy E2E load, and why worker recycling is off).
# Mirrors the args in docker/entrypoints/run-server.sh (1 worker × 20
# gthread threads) to keep parity with production. Multi-worker
# configurations expose timing-sensitive races in the SQL Lab → Explore
# navigation flow under E2E. We diverge from the entrypoint on:
# --timeout 120: heavy dashboard import/export specs exceed the 60s
# default
# superset.app:create_app(): explicit factory so we don't depend on
# FLASK_APP being exported
#
# No --max-requests, matching the entrypoint's default of 0 (recycling
# off). With a single worker a recycle takes the whole backend offline for
# the graceful-timeout drain — browser keep-alive connections hold it open
# for the full 30s — plus ~5s of app boot, which flakes whichever specs
# happen to navigate into the outage. Lowering --graceful-timeout is not
# enough: a dashboard load plus chart render needs 6-10s, which still
# lands inside the window.
nohup gunicorn \
--bind "127.0.0.1:$port" \
--workers 1 \
@@ -345,18 +265,29 @@ playwright-run() {
say "::group::Run Playwright tests"
echo "Running Playwright with baseURL: ${PLAYWRIGHT_BASE_URL}"
if [ -n "$TEST_PATH" ]; then
# Check if there are any test files in the specified path
if ! find "playwright/tests/${TEST_PATH}" -name "*.spec.ts" -type f 2>/dev/null | grep -q .; then
echo "No test files found in ${TEST_PATH} - skipping test run"
# Check if there are any test files in the specified paths
local found=0
local candidate
for candidate in "${TEST_PATHS[@]}"; do
if find "playwright/tests/${candidate}" -name "*.spec.ts" -type f 2>/dev/null | grep -q .; then
found=1
elif [ -f "playwright/tests/${candidate}" ]; then
found=1
fi
done
if [ "$found" -eq 0 ]; then
echo "No test files found in ${TEST_PATHS[*]} - skipping test run"
say "::endgroup::"
return 0
fi
echo "Running tests: ${TEST_PATH}"
npx playwright test "${TEST_PATH}" --output=playwright-results
echo "Running tests: ${TEST_PATHS[*]}"
# shellcheck disable=SC2086
npx playwright test "${TEST_PATHS[@]}" --output=playwright-results ${PLAYWRIGHT_EXTRA_ARGS:-}
local status=$?
else
echo "Running all default-project tests"
npx playwright test --output=playwright-results
echo "Running with no explicit paths (project selection comes from PLAYWRIGHT_EXTRA_ARGS, if set)"
# shellcheck disable=SC2086
npx playwright test --output=playwright-results ${PLAYWRIGHT_EXTRA_ARGS:-}
local status=$?
fi
say "::endgroup::"
@@ -364,6 +295,122 @@ playwright-run() {
return $status
}
playwright-run-gaq() {
# Global Async Queries needs more than a feature flag: submissions are handed
# to Celery, so without a worker consuming the queue the API returns 202 and
# no job ever runs -- the specs would time out rather than fail usefully.
# `playwright-run` boots gunicorn with this step's environment, so the flag
# set on the step reaches both the web server and the worker started here.
local APP_ROOT=$1
cd "$GITHUB_WORKSPACE"
if [ "${SUPERSET_FEATURE_GLOBAL_ASYNC_QUERIES:-}" != "true" ]; then
echo "::error::SUPERSET_FEATURE_GLOBAL_ASYNC_QUERIES must be \"true\" for this step; the specs would skip themselves and report nothing."
return 1
fi
local workerlog="${HOME}/superset-gaq-worker.log"
say "::group::Start Celery worker for GAQ"
# Mirrors docker/docker-bootstrap.sh's worker invocation.
nohup celery --app=superset.tasks.celery_app:app worker \
-O fair \
--loglevel=INFO \
--concurrency=2 \
>"$workerlog" 2>&1 </dev/null &
local workerPid=$!
# Fail fast on a worker that never comes up: a dead worker is
# indistinguishable from a slow one once the specs start timing out.
local timeout=60
while [ $timeout -gt 0 ]; do
if ! kill -0 "$workerPid" 2>/dev/null; then
echo "::error::Celery worker exited during startup"
cat "$workerlog" || true
say "::endgroup::"
return 1
fi
if grep -q "celery@.*ready" "$workerlog" 2>/dev/null; then
say "Celery worker is ready"
break
fi
sleep 1
timeout=$((timeout - 1))
done
if [ $timeout -eq 0 ]; then
echo "::error::Celery worker failed to become ready within 60 seconds"
cat "$workerlog" || true
kill "$workerPid" 2>/dev/null || true
say "::endgroup::"
return 1
fi
say "::endgroup::"
# The GAQ specs are excluded from every other project, so this is what makes
# them loadable at all -- see the chromium-gaq project in playwright.config.ts.
export INCLUDE_GAQ=true
local report="${GITHUB_WORKSPACE}/superset-frontend/playwright-gaq-report.json"
rm -f "$report"
export PLAYWRIGHT_JSON_OUTPUT_NAME="$report"
# --workers=1: the fixtures all create charts as the same admin user, and
# Superset's tag listener races on the shared `editor:<id>` tag (see the
# chromium-gaq project in playwright.config.ts). That project's
# `fullyParallel: false` only orders tests within one file -- Playwright
# still runs separate files concurrently -- and this suite spans three, so
# one worker is what actually serializes it.
#
# --project=chromium-gaq rather than a list of spec paths: that project's
# testMatch (`**/global-async-query*.spec.ts`) is the same glob the default
# and sqllab projects use to *exclude* these specs, so selecting by project
# keeps both sides reading from one definition. A new spec matching the glob
# is picked up here automatically; with a hand-maintained path list it would
# be excluded from the required run and never added here, so it would run
# nowhere and the zero-executed check below could not notice.
export PLAYWRIGHT_EXTRA_ARGS="--reporter=list,json --workers=1 --project=chromium-gaq"
# `set -e` is on: without the guard a failing run would exit before the
# worker log is emitted and before the did-it-actually-run check below.
local status=0
playwright-run "$APP_ROOT" || status=$?
unset PLAYWRIGHT_EXTRA_ARGS PLAYWRIGHT_JSON_OUTPUT_NAME INCLUDE_GAQ
say "::group::Celery worker log"
cat "$workerlog" || true
say "::endgroup::"
kill "$workerPid" 2>/dev/null || true
# A suite that skips itself still exits 0. That is the failure mode this step
# exists to prevent, so assert that tests actually ran.
if [ ! -f "$report" ]; then
echo "::error::No Playwright JSON report produced; cannot confirm the GAQ specs ran."
return 1
fi
# "Did the suite run?" must count every outcome that proves a test executed,
# not just first-attempt passes. A run where every GAQ test genuinely fails
# (say the worker loses Redis mid-run) reports expected=0 too, and blaming
# that on an inactive GLOBAL_ASYNC_QUERIES would point whoever is debugging
# at the wrong layer -- the job is already red from the real failure.
local expected unexpected flaky skipped executed
expected=$(jq '.stats.expected // 0' "$report")
unexpected=$(jq '.stats.unexpected // 0' "$report")
flaky=$(jq '.stats.flaky // 0' "$report")
skipped=$(jq '.stats.skipped // 0' "$report")
executed=$((expected + unexpected + flaky))
say "GAQ suite: ${expected} passed, ${unexpected} failed, ${flaky} flaky, ${skipped} skipped"
if [ "$executed" -eq 0 ]; then
echo "::error::The GAQ specs reported zero executed tests -- they skipped themselves, which means GLOBAL_ASYNC_QUERIES was not active on the server under test."
return 1
fi
if [ "$skipped" -ne 0 ]; then
echo "::error::${skipped} GAQ test(s) skipped; this step must run the whole suite."
return 1
fi
return $status
}
eyes-storybook-dependencies() {
say "::group::install eyes-storyook dependencies"
sudo apt-get update -y && sudo apt-get -y install gconf-service ca-certificates libxshmfence-dev fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libglib2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libnspr4 libnss3 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release xdg-utils libappindicator1

View File

@@ -49,10 +49,4 @@ module.exports = {
// one is built with `npm run build`,
// another is built with `npm run build-instrumented`
'instrumented-assets': assetsConfig,
cypress: {
path: [`${homeDirectory}/.cache/Cypress`],
hashFiles: [
`${workspaceDirectory}/superset-frontend/cypress-base/package-lock.json`,
],
},
};

134
.github/workflows/chromatic.yml vendored Normal file
View File

@@ -0,0 +1,134 @@
# 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.
# Publishes superset-frontend's Storybook to Chromatic for visual
# regression testing. See https://www.chromatic.com/docs/github-actions
#
# Runs on pushes to master (keeps the Chromatic baseline in sync with
# mainline) and on pull requests that touch superset-frontend. Fork PRs
# don't receive CHROMATIC_PROJECT_TOKEN -- GitHub withholds repository
# secrets from pull_request runs triggered by a fork -- so the publish
# steps below no-op for them (via the CHROMATIC_PROJECT_TOKEN != '' guard)
# rather than failing.
#
# Non-blocking for now (exitZeroOnChanges: true): visual changes are
# surfaced as a PR check/comment for review, not enforced as a merge gate.
# A prior Chromatic setup here (#21095) was removed in #27232 for being
# unmaintained and overlapping with Applitools (since also discontinued).
# Keep this one simple and watch it before considering a required check.
#
# The master (push) run auto-accepts its own changes so the baseline stays
# current; otherwise every subsequent PR would keep reporting the same
# already-merged diff against a stale baseline. PR runs are left alone so a
# human still reviews what changed.
name: Chromatic
on:
push:
branches:
- master
paths:
- "superset-frontend/**"
- ".github/workflows/chromatic.yml"
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
paths:
- "superset-frontend/**"
workflow_dispatch: {}
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
TAG: apache/superset:chromatic-${{ github.run_id }}
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
permissions:
contents: read
jobs:
chromatic:
runs-on: ubuntu-26.04
timeout-minutes: 30
# pull-requests: write lets chromaui/action post its check and PR
# comment. Withheld automatically by GitHub for fork-triggered
# pull_request runs, same as CHROMATIC_PROJECT_TOKEN above.
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# TurboSnap (onlyChanged below) needs history to diff against the
# baseline commit, but NOT fetch-depth: 0. This Chromatic project
# was dormant for ~2 years (Chromatic here shipped in #21095,
# removed in #27232) before this workflow, so its last known
# baseline predates thousands of commits on a very active repo.
# With full history, Chromatic's CLI tries to `git log` every
# commit back to that ancient baseline as individual CLI args and
# hits the OS ARG_MAX limit (E2BIG) -- see
# https://github.com/chromaui/chromatic-cli/issues/432, where the
# Chromatic team's own recommended workaround is exactly this: a
# bounded depth, since a multi-year-old baseline isn't useful
# anyway. 500 is far more than any realistic PR needs once a
# recent baseline exists (i.e. after this workflow's own first
# successful run on master).
fetch-depth: 500
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Build Docker Image
if: ${{ env.CHROMATIC_PROJECT_TOKEN != '' }}
run: |
docker buildx build \
-t $TAG \
--cache-from=type=registry,ref=apache/superset-cache:3.11-slim-trixie \
--target superset-node-ci \
.
# --webpack-stats-json writes preview-stats.json into storybook-static.
# TurboSnap (onlyChanged below) needs it to trace which stories a
# changed file affects; without it, chromaui/action fails with "Could
# not retrieve dependent story files" since storybookBuildDir points
# at an already-built Storybook it can't inject its own stats
# collection into.
- name: Build Storybook
if: ${{ env.CHROMATIC_PROJECT_TOKEN != '' }}
run: |
mkdir -p ${{ github.workspace }}/superset-frontend/storybook-static
docker run \
-v ${{ github.workspace }}/superset-frontend/storybook-static:/app/superset-frontend/storybook-static \
--rm $TAG \
bash -c "npm i && npm run build-storybook -- --webpack-stats-json"
- name: Publish to Chromatic
if: ${{ env.CHROMATIC_PROJECT_TOKEN != '' }}
uses: chromaui/action@db3ceb5efb0cd7447d63c2d8cce7da732ce89322 # v18.10.1
with:
projectToken: ${{ env.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
workingDir: superset-frontend
storybookBuildDir: storybook-static
# TurboSnap: only re-snapshot stories affected by files changed
# since the baseline build.
onlyChanged: true
exitZeroOnChanges: true
autoAcceptChanges: ${{ github.event_name == 'push' }}
zip: true

View File

@@ -19,14 +19,13 @@ permissions:
jobs:
changes:
runs-on: ubuntu-26.04
runs-on: ubuntu-slim
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
python: ${{ steps.check.outputs.python }}
frontend: ${{ steps.check.outputs.frontend }}
languages: ${{ steps.check.outputs.languages }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -44,9 +43,9 @@ jobs:
# Skip on PRs that touch neither code group (e.g. docs-only) so the
# analysis runners don't spin up. push/schedule runs always proceed:
# the change-detector returns "all changed" for non-PR events.
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
if: ${{ contains(fromJSON(needs.changes.outputs.languages), 'python') || contains(fromJSON(needs.changes.outputs.languages), 'javascript') }}
runs-on: ubuntu-26.04
timeout-minutes: 30
timeout-minutes: 15
permissions:
actions: read
contents: read
@@ -56,7 +55,7 @@ jobs:
strategy:
fail-fast: false
matrix:
language: ["python", "javascript"]
language: ${{ fromJson(needs.changes.outputs.languages) }}
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
@@ -67,7 +66,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -78,6 +77,6 @@ jobs:
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
category: "/language:${{matrix.language}}"

View File

@@ -482,7 +482,7 @@ jobs:
contains(fromJson('["superset", "lean"]'), matrix.build_preset) &&
steps.grype-scan.outputs.sarif != ''
continue-on-error: true
uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
sarif_file: ${{ steps.grype-scan.outputs.sarif }}
# Both `superset` and `lean` are scanned now; without a distinct

View File

@@ -141,7 +141,7 @@ jobs:
yarn install --check-cache
- name: Download database diagnostics (if triggered by integration tests)
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
uses: dawidd6/action-download-artifact@eab87c9830c39eff17e5a6eadb20bfb4bc880477 # v27
continue-on-error: true
with:
workflow: superset-python-integrationtest.yml
@@ -150,7 +150,7 @@ jobs:
path: docs/src/data/
- name: Try to download latest diagnostics (for push/dispatch triggers)
if: github.event_name != 'workflow_run'
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
uses: dawidd6/action-download-artifact@eab87c9830c39eff17e5a6eadb20bfb4bc880477 # v27
continue-on-error: true
with:
workflow: superset-python-integrationtest.yml

View File

@@ -140,7 +140,7 @@ jobs:
run: |
yarn install --check-cache
- name: Download database diagnostics from integration tests
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
uses: dawidd6/action-download-artifact@eab87c9830c39eff17e5a6eadb20bfb4bc880477 # v27
with:
workflow: superset-python-integrationtest.yml
run_id: ${{ github.event.workflow_run.id }}

View File

@@ -9,10 +9,6 @@ on:
types: [synchronize, opened, reopened, ready_for_review]
workflow_dispatch:
inputs:
use_dashboard:
description: "Use Cypress Dashboard (true/false) [paid service - trigger manually when needed]. You MUST provide a branch and/or PR number below for this to work."
required: false
default: "false"
ref:
description: "The branch or tag to checkout"
required: false
@@ -50,156 +46,6 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
cypress-matrix:
needs: changes
if: (needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true') && github.event.pull_request.draft == false
runs-on: ubuntu-26.04
timeout-minutes: 30
permissions:
contents: read
pull-requests: read
strategy:
# when one test fails, DO NOT cancel the other
# app_root variant, because this will kill Cypress processes
# leaving the Dashboard hanging ...
# https://github.com/cypress-io/github-action/issues/48
fail-fast: false
matrix:
browser: ["chrome"]
app_root: ${{ github.event_name == 'push' && fromJSON('["", "/app/prefix"]') || fromJSON('[""]') }}
env:
SUPERSET_ENV: development
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset
PYTHONPATH: ${{ github.workspace }}
REDIS_PORT: 16379
GITHUB_TOKEN: ${{ github.token }}
# Only use dashboard when explicitly requested via workflow_dispatch
USE_DASHBOARD: ${{ github.event.inputs.use_dashboard == 'true' || 'false' }}
services:
postgres:
image: ghcr.io/apache/superset/ci/postgres:17-alpine
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
ports:
- 15432:5432
redis:
image: ghcr.io/apache/superset/ci/redis:7-alpine
ports:
- 16379:6379
steps:
# -------------------------------------------------------
# Conditional checkout based on context
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
submodules: recursive
# -------------------------------------------------------
- name: Setup Python
uses: $/.github/actions/setup-backend/
- name: Setup postgres
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: setup-postgres
- name: Import test data
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: testdata
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install npm dependencies
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: npm-install
- name: Build javascript packages
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: build-instrumented-assets
- name: Install cypress
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: cypress-install
- name: Run Cypress
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
env:
CYPRESS_BROWSER: ${{ matrix.browser }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
NODE_OPTIONS: "--max-old-space-size=4096"
with:
run: cypress-run-all ${{ env.USE_DASHBOARD }} ${{ matrix.app_root }}
- name: Set safe app root
if: failure()
id: set-safe-app-root
env:
APP_ROOT: ${{ matrix.app_root }}
run: |
SAFE_APP_ROOT=${APP_ROOT//\//_}
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
- name: Upload Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: failure()
with:
path: ${{ github.workspace }}/superset-frontend/cypress-base/cypress/screenshots
name: cypress-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
playwright-tests:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
@@ -215,8 +61,7 @@ jobs:
matrix:
browser: ["chromium"]
# Subdirectory deployment (APPLICATION_ROOT) is a required-to-pass
# dimension, not an optional one, so it runs on every event —
# unlike cypress-matrix above, which only widens on push.
# dimension, so it runs on every event.
app_root: ["", "/app/prefix"]
env:
SUPERSET_ENV: development
@@ -239,7 +84,7 @@ jobs:
- 16379:6379
steps:
# -------------------------------------------------------
# Conditional checkout based on context (same as Cypress workflow)
# Conditional checkout based on context
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -414,44 +259,191 @@ jobs:
${{ github.workspace }}/superset-frontend/test-results/
name: playwright-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
# Stable required-status-check anchors. cypress-matrix and playwright-tests
# are matrix jobs gated on change detection (python || frontend). On a PR
# that touches neither — e.g. a docs-only PR — they are skipped at the job
# level, which happens before matrix expansion, so the per-combination
# contexts (`cypress-matrix (0, chrome)`, `playwright-tests (chromium)`) are
# never produced and branch protection waits on them forever. These
# always-running jobs report a single stable context that passes when the
# underlying matrix job succeeded or was skipped, and fails only on a real
# failure. Require these in .asf.yaml instead of the matrix-expanded names.
# GAQ runs in its own job rather than as a step in playwright-tests above.
# A step with no explicit `if:` implicitly inherits `if: success()`, so
# when GAQ was a step after the other suites in that job, a failure in
# either of those unrelated suites skipped GAQ entirely rather than failing it
# -- silently leaving that commit with zero GAQ coverage instead of a visible
# red check. A separate job can't share that fate: it either runs and reports
# for itself, or it doesn't start (e.g. the environment itself never came up),
# which is the only case where "no GAQ result" is actually the right outcome.
playwright-tests-gaq:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-26.04
timeout-minutes: 30
continue-on-error: true
permissions:
contents: read
pull-requests: read
strategy:
fail-fast: false
matrix:
browser: ["chromium"]
app_root: ["", "/app/prefix"]
env:
SUPERSET_ENV: development
SUPERSET_CONFIG: tests.integration_tests.superset_test_config_gaq
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset
PYTHONPATH: ${{ github.workspace }}
REDIS_PORT: 16379
GITHUB_TOKEN: ${{ github.token }}
services:
postgres:
image: ghcr.io/apache/superset/ci/postgres:17-alpine
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
ports:
- 15432:5432
redis:
image: ghcr.io/apache/superset/ci/redis:7-alpine
ports:
- 16379:6379
steps:
# -------------------------------------------------------
# Conditional checkout based on context
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
submodules: recursive
# -------------------------------------------------------
- name: Setup Python
uses: $/.github/actions/setup-backend/
- name: Setup postgres
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: setup-postgres
- name: Import test data
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: playwright_testdata
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install npm dependencies
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: npm-install
- name: Build javascript packages
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: build-instrumented-assets
- name: Install Playwright
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: playwright-install
- name: Run Playwright (Global Async Queries Tests)
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
env:
NODE_OPTIONS: "--max-old-space-size=4096"
# Scoped to this step for the same reason as the Soft-delete, Embedded
# and Mobile steps in playwright-tests above: GLOBAL_ASYNC_QUERIES
# changes chart-data behaviour for every spec sharing a server.
#
# This suite has its own job, and is not required, because unlike the
# other flag-gated suites it needs a Celery worker: submissions are
# handed to Celery, so without one the API returns 202 and no job ever
# executes. Putting a background worker and its startup window on the
# required path would let a GAQ flake block PRs that have nothing to
# do with async queries.
#
# `playwright-run-gaq` starts the worker and sets INCLUDE_GAQ, which
# is what makes the chromium-gaq project exist at all.
SUPERSET_FEATURE_GLOBAL_ASYNC_QUERIES: "true"
with:
# No spec list: playwright-run-gaq selects --project=chromium-gaq, whose
# testMatch is the single definition of what the GAQ suite contains.
run: playwright-run-gaq "${{ matrix.app_root }}"
- name: Set safe app root
if: failure()
id: set-safe-app-root
env:
APP_ROOT: ${{ matrix.app_root }}
run: |
SAFE_APP_ROOT=${APP_ROOT//\//_}
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
- name: Upload Playwright Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: failure()
with:
path: |
${{ github.workspace }}/superset-frontend/playwright-results/
${{ github.workspace }}/superset-frontend/test-results/
name: playwright-gaq-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
# Stable required-status-check anchor. playwright-tests is a matrix job
# gated on change detection (python || frontend). On a PR that touches
# neither — e.g. a docs-only PR — it is skipped at the job level, which
# happens before matrix expansion, so the per-combination context
# (`playwright-tests (chromium)`) is never produced and branch protection
# waits on it forever. This always-running job reports a single stable
# context that passes when the underlying matrix job succeeded or was
# skipped, and fails only on a real failure. Require it in .asf.yaml instead
# of the matrix-expanded name.
#
# A matrix job reads as "skipped" in two distinct cases, and only the first
# is a legitimate pass: (a) change detection succeeded and gated the job off
# (docs-only PR); (b) the `changes` job itself failed or was cancelled, in
# which case GHA skips its dependents too. Accepting (b) would let a broken
# change-detector report a false green, so each anchor first requires
# change-detector report a false green, so the anchor first requires
# `changes` to have succeeded before honouring a skip.
cypress-matrix-required:
needs: [changes, cypress-matrix]
if: always()
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check cypress-matrix result
env:
CHANGES: ${{ needs.changes.result }}
RESULT: ${{ needs.cypress-matrix.result }}
run: |
if [ "$CHANGES" != "success" ]; then
echo "change detection did not succeed (result: $CHANGES); refusing to pass on a skipped matrix"
exit 1
fi
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
echo "cypress-matrix did not pass (result: $RESULT)"
exit 1
fi
echo "cypress-matrix result: $RESULT (changes: $CHANGES)"
playwright-tests-required:
needs: [changes, playwright-tests]
if: always()
@@ -475,7 +467,7 @@ jobs:
echo "playwright-tests result: $RESULT (changes: $CHANGES)"
actions-timeline:
needs: [cypress-matrix, playwright-tests, cypress-matrix-required, playwright-tests-required]
needs: [playwright-tests, playwright-tests-required]
if: always()
runs-on: ubuntu-slim
permissions:

View File

@@ -43,38 +43,29 @@ jobs:
with:
install-superset: "false"
# Still vendored (not de-vendored like chart-releaser-action below): the
# allowlisted helm/chart-testing-action@v2.8.0 depends internally on
# astral-sh/setup-uv@v7.0.0, which isn't itself on the ASF Actions
# allowlist (only v8.1.0+ are, at apache/infrastructure-actions'
# actions.yml). Needs an INFRA request before this can de-vendor too.
# chart-testing-action is a submodule (not a plain directory), and the
# $/ self-repository syntax resolves action files directly from the
# repository without performing a real (submodule-aware) checkout, so
# it can't see into a submodule's link. Keep this one on the
# workspace-relative ./ form, consistent with every other workflow in
# the repo that references a submodule action.
- name: Set up chart-testing
uses: ./.github/actions/chart-testing-action # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0
- name: Run chart-testing (list-changed)
id: list-changed
run: |
changed=$(ct list-changed --print-config)
changed=$(ct list-changed --target-branch ${GITHUB_EVENT_REPOSITORY_DEFAULT_BRANCH} --print-config)
if [[ -n "$changed" ]]; then
echo "changed=true" >> $GITHUB_OUTPUT
fi
env:
CT_CHART_DIRS: helm
CT_SINCE: HEAD
GITHUB_EVENT_REPOSITORY_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
- name: Run chart-testing (lint)
run: ct lint --print-config
run: ct lint --target-branch ${GITHUB_EVENT_REPOSITORY_DEFAULT_BRANCH} --print-config
env:
CT_CHART_DIRS: helm
CT_LINT_CONF: lintconf.yaml
CT_SINCE: HEAD
CT_CHART_REPOS: bitnami=https://charts.bitnami.com/bitnami
GITHUB_EVENT_REPOSITORY_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
- name: Set up helm-unittest
run: helm plugin install https://github.com/helm-unittest/helm-unittest --version v1.1.1

View File

@@ -60,6 +60,8 @@ jobs:
run: |
npm run build-translation
# This job id is a required status check in .asf.yaml; renaming it breaks
# branch protection for every PR until .asf.yaml is updated to match.
babel-extract:
runs-on: ubuntu-26.04
permissions:

View File

@@ -71,6 +71,7 @@ jobs:
{"dialect": "trino", "timeout": 10},
{"dialect": "mssql", "timeout": 10},
{"dialect": "elasticsearch", "timeout": 10},
{"dialect": "opensearch", "timeout": 10},
{"dialect": "oracle", "timeout": 15},
{"dialect": "db2", "timeout": 25},
{"dialect": "mariadb", "timeout": 10},

3
.gitmodules vendored
View File

@@ -21,9 +21,6 @@
[submodule ".github/actions/cached-dependencies"]
path = .github/actions/cached-dependencies
url = https://github.com/apache-superset/cached-dependencies
[submodule ".github/actions/chart-testing-action"]
path = .github/actions/chart-testing-action
url = https://github.com/helm/chart-testing-action
[submodule ".github/actions/github-action-push-to-another-repository"]
path = .github/actions/github-action-push-to-another-repository
url = https://github.com/cpina/github-action-push-to-another-repository

View File

@@ -105,10 +105,9 @@ repos:
files: ^docs/.*\.(js|jsx|ts|tsx)$
- id: type-checking-frontend
name: Type-Checking (Frontend)
entry: ./scripts/check-type.js package=superset-frontend excludeDeclarationDir=cypress-base
entry: ./scripts/check-type.js package=superset-frontend
language: system
files: ^superset-frontend\/.*\.(js|jsx|ts|tsx)$
exclude: ^superset-frontend/cypress-base\/
require_serial: true
# blacklist unsafe functions like make_url (see #19526)
- repo: https://github.com/skorokithakis/blacklist-pre-commit-hook

View File

@@ -44,8 +44,7 @@ Common pre-commit failures:
### Testing Strategy Migration
- **Prefer unit tests** over integration tests
- **Prefer integration tests** over end-to-end tests
- **Use Playwright for E2E tests** - Migrating from Cypress
- **Cypress is deprecated** - Will be removed once migration is completed
- **Use Playwright for E2E tests** - Cypress has been removed
- **Use Jest + React Testing Library** for component testing
- **Use `test()` instead of `describe()`** - Follow [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principles
@@ -202,18 +201,13 @@ The Developer Portal auto-generates MDX documentation from Storybook stories. **
npm run test # All tests
npm run test -- filename.test.tsx # Single file
# E2E Tests (Playwright - NEW)
# E2E Tests (Playwright)
npm run playwright:test # All Playwright tests
npm run playwright:ui # Interactive UI mode
npm run playwright:headed # See browser during tests
npx playwright test tests/auth/login.spec.ts # Single file
npm run playwright:debug tests/auth/login.spec.ts # Debug specific file
# E2E Tests (Cypress - DEPRECATED)
cd superset-frontend/cypress-base
npm run cypress-run-chrome # All Cypress tests (headless)
npm run cypress-debug # Interactive Cypress UI
# Backend
pytest # All tests
pytest tests/unit_tests/specific_test.py # Single file

View File

@@ -195,17 +195,15 @@ RUN mkdir -p \
requirements \
&& touch superset/static/version_info.json
# Install Playwright and optionally setup headless browsers
# Optionally install Playwright and the headless Chromium used for screenshots
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/playwright-browsers
ARG INCLUDE_CHROMIUM="false"
ARG INCLUDE_FIREFOX="false"
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
if [ "${INCLUDE_CHROMIUM}" = "true" ] || [ "${INCLUDE_FIREFOX}" = "true" ]; then \
if [ "${INCLUDE_CHROMIUM}" = "true" ]; then \
uv pip install playwright && \
playwright install-deps && \
if [ "${INCLUDE_CHROMIUM}" = "true" ]; then playwright install chromium; fi && \
if [ "${INCLUDE_FIREFOX}" = "true" ]; then playwright install firefox; fi; \
playwright install chromium; \
else \
echo "Skipping browser installation"; \
fi

View File

@@ -103,14 +103,6 @@ flask-app:
node-app:
cd superset-frontend; npm run dev-server
build-cypress:
cd superset-frontend; npm run build-instrumented
cd superset-frontend/cypress-base; npm ci
open-cypress:
if ! [ $(port) ]; then cd superset-frontend/cypress-base; CYPRESS_BASE_URL=http://localhost:9000 npm run cypress open; fi
cd superset-frontend/cypress-base; CYPRESS_BASE_URL=http://localhost:$(port) npm run cypress open
report-celery-worker:
celery --app=superset.tasks.celery_app:app worker

View File

@@ -40,10 +40,6 @@ under the License.
| can read on Database |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can write on Database |:heavy_check_mark:|O|O|O|O|
| can read on Query |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can this form get on ResetPasswordView |:heavy_check_mark:|O|O|O|O|
| can this form post on ResetPasswordView |:heavy_check_mark:|O|O|O|O|
| can this form get on ResetMyPasswordView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can this form post on ResetMyPasswordView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can this form get on UserInfoEditView |:heavy_check_mark:|O|O|O|O|
| can this form post on UserInfoEditView |:heavy_check_mark:|O|O|O|O|
| can show on UserDBModelView |:heavy_check_mark:|O|O|O|O|
@@ -52,8 +48,6 @@ under the License.
| can add on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| can list on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| can userinfo on UserDBModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| resetmypassword on UserDBModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| resetpasswords on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| userinfoedit on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| can show on RoleModelView |:heavy_check_mark:|O|O|O|O|
| can edit on RoleModelView |:heavy_check_mark:|O|O|O|O|

View File

@@ -24,6 +24,60 @@ assists people when migrating to a new version.
## Next
- Example export (`/export_as_example/`) rejects dashboards whose charts or
native-filter targets use semantic views; use the ordinary chart/dashboard
bundle export instead.
- Semantic-view chart and dashboard bundles use typed UUID references and require
a destination with support for this format and an already provisioned, accessible
view with the same UUID. They do not provision semantic layers/views or export
their configuration or credentials. Older readers cannot import these semantic
bundles. Older dashboard bundles with semantic filter targets but no typed
`datasourceRef` are also rejected; re-export them with this version rather than
binding their local IDs to unrelated destination tables. Imports fail explicitly
if a semantic dependency is missing, inaccessible, feature-disabled or
provider-unregistered. Chart and dashboard exports omit references for missing
views so unrelated assets can still be archived; orphaned charts and dashboard
targets cannot be imported without repairing their datasources. Existing-view
access, feature and provider checks remain enforced, including in full asset
exports. Ordinary table bundles retain their existing format. The examples
loader rejects semantic bundles;
use the chart, dashboard or assets importer instead.
### Semantic-view Table charts without a temporal axis
Semantic-view Table charts omit recognized dormant time grains from
frontend-generated aggregate queries when no temporal axis is present. The saved
grain and Time Grain control visibility are unchanged. Direct API payloads and
saved chart-data GET requests that bypass frontend rebuilding retain strict
validation; some old stored query contexts can therefore still fail. Deploy
updated frontend assets with this change.
### SQLite time filters on `DATE` columns
On SQLite, Shillelagh and the Superset meta database, a time filter on a `DATE`
column writes its bounds as a date, such as `'2026-09-20'`, and no longer uses
the column's **Datetime format** (`python_date_format`) or the database's
`python_date_format_by_column_name`. This fixes ranges that started and ended
one day late on `DATE` columns holding `YYYY-MM-DD` text. A `DATE` column that
holds values in another format, such as `20260920`, `09/20/2026` or epoch
seconds, now matches no rows, even with a Datetime format set. Columns declared
as `INTEGER` are not affected. On Shillelagh and the meta database, a bound
with a time of day is cut to its date.
### Apache Doris connection form and `DBS_AVAILABLE_DENYLIST`
`DBS_AVAILABLE_DENYLIST` is matched against an engine spec's `default_driver`.
The Doris engine spec's `default_driver` is `mysqldb` (the driver pydoris
registers), so a deployment that hides Doris with `{"pydoris": {"pydoris"}}`
sees it listed again; change the entry to `{"pydoris": {"mysqldb"}}`.
The Doris connection form's SSL switch sets `ssl_mode=VERIFY_CA`. With MariaDB
Connector/C this also verifies the server hostname, so the switch needs
`ssl_ca=<path>` in Additional parameters and a Doris FE certificate matching the
host. A Doris FE on its default self-signed certificate fails to connect once
the switch is on.
### Empty MCP chart previews
Saved Bubble and Histogram Vega-Lite previews with zero rows return an empty
@@ -67,6 +121,32 @@ format. Use a consistent timezone and precision for stored strings and bounds.
able to provision a user regardless of `AUTH_USER_REGISTRATION`; it now
requires the same gate as `/register/`.
### SQL execution request limits are safety caps
The MCP `execute_sql` request's `limit` no longer overrides a smaller outer SQL
LIMIT. It caps the last statement at the smaller of the SQL LIMIT and the request
limit, subject to `SQL_MAX_ROW`. For example, SQL `LIMIT 5` with request `limit: 10`
keeps `LIMIT 5`. To return more rows, increase or remove the SQL LIMIT explicitly.
Omitting the request limit still leaves SQL limits unchanged. Inner query and CTE
limits are not changed.
This contract change also applies to the public `Database.execute()` and
`Database.execute_async()` APIs through `QueryOptions.limit`, including dry runs.
SQL Lab also recognizes literal `FETCH FIRST` counts and parenthesized literal
limits. SQL Server `TOP ... PERCENT` and `TOP ... WITH TIES` are no longer
interpreted as fixed row counts: SQL Lab uses the row-limit dropdown (subject to
server limits) and rewrites these clauses to a fixed `TOP` cap. For example,
`TOP 5 PERCENT` with a 1000-row dropdown can return up to 1000 rows rather than
the previous five.
`BigQueryEngineSpec.fetch_data` no longer falls back to a plain `fetchall()` once
it has read its initial sample, since a forward-only cursor cannot replay those
rows. A BigQuery driver error after the sample now fails the query instead of
returning the remaining rows as a success. This applies to every caller,
including legacy SQL Lab execution and dataset column discovery. An error on the
initial read still falls back as before.
### Doris SSL requests require TLS
The Doris SSL toggle uses `ssl_mode=VERIFY_CA`. Saved `ssl_mode=REQUIRED` and
@@ -126,6 +206,26 @@ get the same handling. With MariaDB Connector/Python
ssl_verify_cert=True. Other MySQL-compatible engines such as OceanBase and
StarRocks keep their existing SSL handling.
### SQL Lab decimal results use exact strings
SQL Lab represents database `DECIMAL`/`NUMERIC` values as JSON strings instead
of JSON numbers, preserving precision and trailing zeros in the results grid
and exports. The strings use fixed-point notation, and non-finite `NaN` and
`Infinity` decimals are returned as `null`. Numeric sorting still compares
their exact values. API consumers performing arithmetic should parse these
strings with a decimal library, not JavaScript `Number`. This includes SQL Lab
extensions: the `data` rows passed to `sqlLab.onDidQuerySuccess` listeners
carry strings for `numeric` columns (for example PostgreSQL `SUM(bigint)`,
`AVG` or `ROUND` results), so adding them with `+` concatenates instead of
summing. The `sqleditor.extension.resultTable` override, which replaces
`FilterableTable` in both SQL Lab results and table previews, also receives
string rows but not the built-in exact-decimal comparator; extensions must
implement their own decimal sorting. Chart and dataset queries also use fixed-point strings for Decimal
columns that fall back to string conversion (for example, Decimals mixed with
floats), and non-finite Decimals in those columns become null. Other chart
result serialization and chart number formatting are unchanged. Re-run queries
whose cached JSON results were produced before upgrading all workers.
### Version history retention setting
Use `VERSION_HISTORY_RETENTION_DAYS` for both the application setting and
@@ -308,6 +408,15 @@ upgrading. See the two migrations' docstrings (`superset/migrations/versions/
`..._00-01_3ce9a4572f8a_rename_deprecated_permissions_33272.py`) for the full
per-permission mapping and reasoning.
- Snowflake stage file-management statements (`PUT`, `GET`, `REMOVE` and its
`RM` alias) are rejected in user-submitted SQL (SQL Lab, the cost-estimate
path and alert queries), regardless of the database's `allow_dml` setting.
`PUT`/`GET` perform file I/O on the host running the
query, and `REMOVE`/`RM` delete files within a stage; none of them read or
write table data, so `allow_dml` does not govern them. Deployments that ran
these through SQL Lab should manage stage files with Snowflake's own clients
instead. `LIST`/`LS`, which only enumerate staged files, are unaffected.
### MySQL metadata database now actually defaults to READ COMMITTED
Superset has always *intended* to default the metadata-database isolation
@@ -338,6 +447,36 @@ This fixed embedding cap is independent of the operator's
`MCP_RESPONSE_SIZE_CONFIG['max_bytes']` (50,000 by default); it does not guarantee
that every payload fits a configured response limit.
### Legacy FAB password reset pages are removed
The Flask-AppBuilder server-rendered password reset pages at
`/resetpassword/form` (admin reset of another account) and
`/resetmypassword/form` (self-service reset) are no longer registered; both
routes answer 404, and the "Reset Password" and "Reset my password" buttons on
the legacy FAB user pages that led to them are gone. `superset init` no longer
assigns their permissions (`can this form get/post on ResetPasswordView` and
`ResetMyPasswordView`, plus the `resetpasswords` and `resetmypassword` actions on
`UserDBModelView`) to any role. Every flow they served lives in the SPA:
administrators reset a user's password from the "New password" fields in the
Users list edit modal (`PUT /api/v1/security/users/<id>`), users change their
own from the "Reset my password" modal on their profile page (`PUT
/api/v1/me/`, which requires `current_password` when the account already has one), and a pending forced password
change (`ENABLE_FORCE_PASSWORD_CHANGE`) now redirects to that profile page
instead of the removed form. Deployments that link to either legacy route should
point at `/user_info/` or the Users list instead.
### Unused `INCLUDE_FIREFOX` build arg and dead config removed
Screenshots use only Playwright with Chromium, so the `INCLUDE_FIREFOX` Docker build
arg, which installed a Firefox browser that nothing used, has been removed from the
`Dockerfile` and the `docker-compose` files. Builds that still pass `INCLUDE_FIREFOX`
keep working, though Docker may warn that the build argument is unused. Use
`INCLUDE_CHROMIUM=true` to add the browser that screenshots need.
The `EMAIL_PAGE_RENDER_WAIT` config key and the `ENABLE_PLAYWRIGHT` variable in
`docker/.env` are also removed. Nothing in Superset read either of them, so setting
them had no effect and they can be deleted from custom configs.
### Default Docker image is now batteries-included; the minimal image moves to `-lean`
The default `apache/superset` Docker image (the plain tags: `latest`, `master`,
@@ -804,6 +943,54 @@ substitution is deliberately not blanked so the broken link is visible rather th
silently truncated. Use `{datasource_id}` (still supported) to identify the dataset
to your access-request system, and resolve the name there.
### Semantic member selection versions
Semantic providers can declare `selection_identity_version` when changing their
member identity encoding. Cube's stable-member-ID adapter requires
`cube-member-id-v1`. Deploy the compatible core library, host, frontend and Cube
adapter together; do not roll back to title-keyed code after saving ID-based charts.
Saved Cube selections without this version are rejected, even when a saved title
happens to equal a member ID. No historical title mapping is recovered. Open each
chart in Explore, choose **Start field selection**, reselect its metrics,
dimensions, filters and sorting, then save. Reset also clears field-dependent
formatting; it does not overwrite the saved chart until Save is chosen. Native
filters and chart customizations have a separate reset in their configuration
form that clears fields, pre-filters, sorting, defaults and dependencies.
Dashboard filters retain their own saved generation. Old filter state and
unversioned external member overrides cannot borrow a chart's version. Recreate
old dashboard permalink/filter state after reselection. Cross-view overlays are
rejected unless their source identity matches the receiving semantic view;
matching display titles are not proof of matching members.
Dynamic group-by display controls are unsupported on versioned semantic views,
including Cube. Remove those charts from the control's scope; resetting or
reselecting the group-by fields cannot repair this control's missing selection
identity evidence. Static chart group-by selections remain available after the
chart's explicit reselection. Supporting dynamic group-by requires preserving the
version in target rebuilds and recording the source identity in the emitted mask;
that follow-up is not included here.
Matrixify All mode with A to Z or Z to A sorting cannot populate or refresh values
for versioned semantic views. Saved values are preserved; for a new selection,
switch to Members and enter values manually. The control explains this limitation
when suggestions are unavailable.
The marker is a compatibility contract, not an authorization credential. API
clients must rebuild their selections from current member IDs before supplying
`extras.semantic_selection_version` on each query. Providers without a required
version retain their existing behavior. No metadata-database migration or
automatic rewrite of saved charts is performed.
Programmatic clients must explicitly select current member IDs. Chart-data API
queries send `extras.semantic_selection_version`; name-based datasource queries
and MCP `get_table` requests send top-level `semantic_selection_version`.
Discover the value from datasource metadata (or each external metric returned by
MCP `list_metrics`). The server never adds a missing marker from metadata.
Explore query controls remain unavailable until explicit field initialization,
so legacy filter subjects cannot trigger value suggestions before reselection.
### MCP tool results preserve stored string values
Structured MCP tool results no longer add `<UNTRUSTED-CONTENT>` wrappers or

View File

@@ -75,7 +75,6 @@ x-common-build: &common-build
args:
DEV_MODE: "true"
INCLUDE_CHROMIUM: ${INCLUDE_CHROMIUM:-false}
INCLUDE_FIREFOX: ${INCLUDE_FIREFOX:-false}
BUILD_TRANSLATIONS: ${BUILD_TRANSLATIONS:-false}
services:

View File

@@ -43,7 +43,6 @@ x-common-build: &common-build
args:
DEV_MODE: "true"
INCLUDE_CHROMIUM: ${INCLUDE_CHROMIUM:-false}
INCLUDE_FIREFOX: ${INCLUDE_FIREFOX:-false}
BUILD_TRANSLATIONS: ${BUILD_TRANSLATIONS:-false}
services:
@@ -125,8 +124,6 @@ services:
restart: unless-stopped
ports:
- ${SUPERSET_PORT:-8088}:8088
# When in cypress-mode ->
- ${CYPRESS_PORT:-8081}:8081
extra_hosts:
- "host.docker.internal:host-gateway"
user: *superset-user

View File

@@ -26,7 +26,6 @@ DEV_MODE=true
# SUPERSET_PORT=8088
# NODE_PORT=9000
# WEBSOCKET_PORT=8080
# CYPRESS_PORT=8081
# DATABASE_PORT=5432
# REDIS_PORT=6379
@@ -79,12 +78,10 @@ SUPERSET_LOAD_EXAMPLES=yes
# examples anyway, e.g. after changing the example datasets or after a partial
# load.
#SUPERSET_FORCE_LOAD_EXAMPLES=no
CYPRESS_CONFIG=false
SUPERSET_PORT=8088
MAPBOX_API_KEY=''
# Make sure you set this to a unique secure random value on production
SUPERSET_SECRET_KEY=TEST_NON_DEV_SECRET
ENABLE_PLAYWRIGHT=false
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
BUILD_SUPERSET_FRONTEND_IN_DOCKER=true

View File

@@ -31,7 +31,6 @@ NGINX_PORT=81
SUPERSET_PORT=8089
NODE_PORT=9001
WEBSOCKET_PORT=8082
CYPRESS_PORT=8083
DATABASE_PORT=5433
REDIS_PORT=6380

View File

@@ -34,13 +34,6 @@ if [ "$DEV_MODE" == "true" ]; then
fi
REQUIREMENTS_LOCAL="/app/docker/requirements-local.txt"
PORT=${PORT:-8088}
# If Cypress run – overwrite the password for admin and export env variables
if [ "$CYPRESS_CONFIG" == "true" ]; then
export SUPERSET_TESTENV=true
export POSTGRES_DB=superset_cypress
export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset_cypress
PORT=8081
fi
# Skip postgres requirements installation for workers to avoid conflicts
if [[ "$DATABASE_DIALECT" == postgres* ]] && [ "$(whoami)" = "root" ] && [ "$1" != "worker" ] && [ "$1" != "beat" ]; then
# older images may not have the postgres dev requirements installed

View File

@@ -36,13 +36,6 @@ Init Step ${1}/${STEP_CNT} [${2}] -- ${3}
EOF
}
ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin}"
# If Cypress run – overwrite the password for admin and export env variables
if [ "$CYPRESS_CONFIG" == "true" ]; then
ADMIN_PASSWORD="general"
export SUPERSET_TESTENV=true
export POSTGRES_DB=superset_cypress
export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset_cypress
fi
# Initialize the database
echo_step "1" "Starting" "Applying DB migrations"
superset db upgrade
@@ -50,16 +43,12 @@ echo_step "1" "Complete" "Applying DB migrations"
# Create an admin user
echo_step "2" "Starting" "Setting up admin user ( admin / $ADMIN_PASSWORD )"
if [ "$CYPRESS_CONFIG" == "true" ]; then
superset load_test_users
else
superset fab create-admin \
--username admin \
--email admin@superset.com \
--password "$ADMIN_PASSWORD" \
--firstname Superset \
--lastname Admin
fi
superset fab create-admin \
--username admin \
--email admin@superset.com \
--password "$ADMIN_PASSWORD" \
--firstname Superset \
--lastname Admin
echo_step "2" "Complete" "Setting up admin user"
# Create default roles and permissions
echo_step "3" "Starting" "Setting up roles and perms"
@@ -97,12 +86,8 @@ PY
if [ "$SUPERSET_LOAD_EXAMPLES" = "yes" ]; then
echo_step "4" "Starting" "Loading examples"
# Cypress runs always load, since they need a distinct set of test data
# (`--load-test-data`) in a separate database. Set
# SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload the examples regardless.
if [ "$CYPRESS_CONFIG" == "true" ]; then
superset load_examples --load-test-data
elif [ "$SUPERSET_FORCE_LOAD_EXAMPLES" != "yes" ] && examples_already_loaded; then
# Set SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload the examples regardless.
if [ "$SUPERSET_FORCE_LOAD_EXAMPLES" != "yes" ] && examples_already_loaded; then
echo "Examples already loaded, skipping (set SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload them)"
else
superset load_examples

View File

@@ -22,7 +22,6 @@
#
import logging
import os
import sys
from celery.schedules import crontab
from flask_caching.backends.filesystemcache import FileSystemCache
@@ -139,18 +138,6 @@ SQLLAB_CTAS_NO_LIMIT = True
log_level_text = os.getenv("SUPERSET_LOG_LEVEL", "INFO")
LOG_LEVEL = getattr(logging, log_level_text.upper(), logging.INFO)
if os.getenv("CYPRESS_CONFIG") == "true":
# When running the service as a cypress backend, we need to import the config
# located @ tests/integration_tests/superset_test_config.py
base_dir = os.path.dirname(__file__)
module_folder = os.path.abspath(
os.path.join(base_dir, "../../tests/integration_tests/")
)
sys.path.insert(0, module_folder)
from superset_test_config import * # noqa
sys.path.pop(0)
#
# Optionally import superset_config_docker.py (which will have been included on
# the PYTHONPATH) in order to allow for local settings to be overridden

View File

@@ -50,13 +50,11 @@ Screenshots will be taken but no messages actually sent as long as `ALERT_REPORT
#### In your `Dockerfile`
You'll need to extend the Superset image to include a headless browser. Your options include:
As of 7.0.0, Superset takes screenshots only with [Playwright](https://playwright.dev/python/) driving a headless Chromium browser. Selenium, Firefox, geckodriver and the `WEBDRIVER_TYPE` setting are not supported, and no feature flag is needed to use Playwright. Prior to 7.0.0, users were responsible for installing their own headless browser, and could use Selenium if desired.
- Use Playwright with Chromium: this is the recommended approach as of version 4.1.x or greater. Playwright always uses Chromium — the `WEBDRIVER_TYPE` config setting has no effect when Playwright is active. A working example of a Dockerfile that installs these tools is provided under "Building your own production Docker image" on the [Docker Builds](/admin-docs/installation/docker-builds#building-your-own-production-docker-image) page. Enable the `PLAYWRIGHT_REPORTS_AND_THUMBNAILS` feature flag in your config to activate it.
- Use Firefox (Selenium): you'll need to install geckodriver and Firefox. Set `WEBDRIVER_TYPE` to `"firefox"` in your `superset_config.py`.
- Use Chrome (Selenium): you'll need to install Chrome. Set `WEBDRIVER_TYPE` to `"chrome"` in your `superset_config.py`.
In Superset versions &lt;=4.0x, users installed Firefox or Chrome and that was documented here.
- The default Superset image (for example the `latest` and `<version>` tags) includes Playwright and Chromium.
- The `-lean` and `-dev` images do not. To add them to an image based on `-lean`, see the example Dockerfile under "Building your own production Docker image" on the [Docker Builds](/admin-docs/installation/docker-builds#building-your-own-production-docker-image) page.
- To build the `dev` target from source with Chromium, pass `--build-arg INCLUDE_CHROMIUM=true`. With Docker Compose, run `INCLUDE_CHROMIUM=true docker compose build`.
Only the worker container needs the browser.
@@ -388,20 +386,13 @@ SMTP_PASSWORD = "your_password" # use the empty string "" if using an unauthenti
SMTP_MAIL_FROM = "noreply@youremail.com"
EMAIL_REPORTS_SUBJECT_PREFIX = "[Superset] " # optional - overwrites default value in config.py of "[Report] "
# WebDriver configuration
# If you use Firefox or Playwright with Chrome, you can stick with default values
# If you use Chrome and are *not* using Playwright, then add the following WEBDRIVER_TYPE and WEBDRIVER_OPTION_ARGS
WEBDRIVER_TYPE = "chrome"
WEBDRIVER_OPTION_ARGS = [
"--force-device-scale-factor=2.0",
"--high-dpi-support=2.0",
"--headless",
"--disable-gpu",
"--disable-dev-shm-usage",
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-extensions",
]
# Screenshot browser configuration
# Playwright always launches Chromium, so there is no browser type to set, and the
# defaults work for most deployments. The values below are the defaults. To render
# sharper screenshots, raise pixel_density in WEBDRIVER_WINDOW. Extra Chromium
# launch arguments can be passed with WEBDRIVER_OPTION_ARGS.
# WEBDRIVER_WINDOW = {"dashboard": (1600, 2000), "slice": (3000, 1200), "pixel_density": 1}
# WEBDRIVER_OPTION_ARGS = []
# This is for internal use, you can keep http
WEBDRIVER_BASEURL = "http://superset:8088" # This is also the default for Docker Compose, where the app service is named "superset"
@@ -429,10 +420,11 @@ Please refer to `ExecutorType` in the codebase for other executor types.
**Important notes**
- Be mindful of the concurrency setting for celery (using `-c 4`). Selenium/webdriver instances can
consume a lot of CPU / memory on your servers.
- In some cases, if you notice a lot of leaked geckodriver processes, try running your celery
processes with `celery worker --pool=prefork --max-tasks-per-child=128 ...`
- Be mindful of the concurrency setting for celery (using `-c 4`). Each worker process keeps its own
headless Chromium instance, and these can consume a lot of CPU / memory on your servers.
- If worker memory grows over time, try running your celery processes with
`celery worker --pool=prefork --max-tasks-per-child=128 ...` so that worker processes, and the
Chromium instances they own, are recycled periodically.
- It is recommended to run separate workers for the `sql_lab` and `email_reports` tasks. This can be
done using the `queue` field in `task_annotations`.
- Adjust `WEBDRIVER_BASEURL` in your configuration file if celery workers can’t access Superset via
@@ -610,11 +602,22 @@ omitted. Cookies, authentication headers, URLs, SQL, and query payloads are not
included in these transport diagnostics. HTTP 400 therefore remains a failure to
investigate, not a reason to repeat the same request.
### Check web browser and webdriver installation
### Check the Playwright and Chromium installation
To take a screenshot, the worker visits the dashboard or chart using a headless browser, then takes a screenshot. If you are able to send a chart as CSV, XLSX, or text but can't send as PNG, your problem may lie with the browser.
To take a screenshot, the worker visits the dashboard or chart using a headless Chromium browser controlled by Playwright, then takes a screenshot. If you are able to send a chart as CSV, XLSX, or text but can't send as PNG, your problem may lie with the browser. If the Playwright package isn't installed, screenshot attempts fail with an error that includes `Playwright is required for screenshots`. If Playwright is installed but Chromium is missing or fails to launch, the error includes `Failed to launch the headless browser for screenshots`, followed by the underlying launch error.
If you are handling the installation of the headless browser on your own, do your own verification to ensure that the headless browser opens successfully in the worker environment.
If you are handling the installation of the headless browser on your own, confirm that Chromium launches in the worker environment. Start Python in your worker environment and run:
```python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
print(browser.version)
browser.close()
```
This should print the Chromium version.
### Send a test email

View File

@@ -241,7 +241,9 @@ FEATURE_FLAGS = {
}
```
By default thumbnails are rendered per user, and will fall back to the Selenium user for anonymous users.
By default thumbnails are rendered as the user who requests them. Anonymous requests have no user
to render as, so they get no thumbnail unless you add a fallback executor, for example
`THUMBNAIL_EXECUTORS = [ExecutorType.CURRENT_USER, FixedExecutor("admin")]`.
To always render thumbnails as a fixed user (`admin` in this example), use the following configuration:
```python
@@ -286,7 +288,7 @@ THUMBNAIL_CACHE_CONFIG = init_thumbnail_cache
```
Using the above example cache keys for dashboards will be `superset_thumb__dashboard__{ID}`. You can
override the base URL for Selenium using:
override the base URL that the headless browser uses to reach Superset using:
```
WEBDRIVER_BASEURL = "https://superset.company.com"
@@ -311,21 +313,40 @@ CACHE_WARMUP_EXECUTORS = [FixedExecutor("admin")]
Use a dedicated read-only service account here rather than a personal admin account, so that
thumbnail rendering and cache warmup tasks don't fail if a specific user's credentials change.
Additional Selenium WebDriver configuration can be set using `WEBDRIVER_CONFIGURATION`. You can
implement a custom function to authenticate Selenium. The default function uses the `flask-login`
session cookie. Here's an example of a custom function signature:
Thumbnails are rendered with Playwright and a headless Chromium browser. Extra Chromium launch
arguments can be set using `WEBDRIVER_OPTION_ARGS`. You can implement a custom function to
authenticate the Playwright browser context. The default function uses the `flask-login` session
cookie. A custom function replaces that default, so it must authenticate as `user` itself.
Here's an example of a custom function:
```python
def auth_driver(driver: WebDriver, user: "User") -> WebDriver:
pass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from flask_appbuilder.security.sqla.models import User
from playwright.sync_api import BrowserContext
def auth_browser_context(
browser_context: "BrowserContext", user: "User"
) -> "BrowserContext":
# This replaces Superset's default session-cookie login, so it must
# authenticate as `user` itself. For example, send a bearer token:
# `get_token_for_user` is a placeholder for your own token lookup.
token = get_token_for_user(user)
browser_context.set_extra_http_headers({"Authorization": f"Bearer {token}"})
return browser_context
```
Then on configuration:
```
WEBDRIVER_AUTH_FUNC = auth_driver
WEBDRIVER_AUTH_FUNC = auth_browser_context
```
To replace the authentication logic entirely, subclass `superset.utils.machine_auth.MachineAuthProvider`,
override `authenticate_browser_context()`, and set `MACHINE_AUTH_PROVIDER_CLASS` to the fully-qualified dotted path of your class (for example, `"myapp.auth.MyMachineAuthProvider"`).
## ETag Support for Thumbnails
Thumbnail and screenshot endpoints return `ETag` response headers based on the cached content digest. Clients can use conditional requests to avoid downloading unchanged images:

View File

@@ -352,7 +352,7 @@ Routes on the health blueprint, including `/health`, `/healthcheck`, `/ping`, an
`/version`, are exempt from forced password changes. Even with
`ENABLE_FORCE_PASSWORD_CHANGE = True`, a probe carrying a session cookie for a user
with a pending password change receives the route's normal response rather than a
redirect to the password-reset form. This exemption does not bypass other security
redirect to the profile page's password change. This exemption does not bypass other security
checks or change which version details the user may see.
If the load balancer is inserting `X-Forwarded-For/X-Forwarded-Proto` headers, you should set

View File

@@ -174,9 +174,9 @@ scheduled table in the warehouse so each chart query is a cheap lookup.
- See [Feature Flags](./feature-flags.mdx) for the full list of supported
flags and their lifecycle stages.
- Server-side screenshot jobs (alerts, scheduled reports, thumbnails)
render the dashboard in a headless, webdriver-controlled browser, which
render the dashboard in a headless Chromium browser driven by Playwright, which
intentionally bypasses row virtualization so the rendered artifact
includes every chart, not just the ones above the fold. User-triggered
includes every chart on the active tab, not just the ones above the fold. User-triggered
"download as image/PDF" is different: it captures whatever's currently
rendered in the user's own browser, so it's still subject to
virtualization like any other page view. Metadata/YAML dashboard export

View File

@@ -0,0 +1,49 @@
---
title: Google Sheets credentials
---
{/*
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.
*/}
## Service-account connections
When configuring Google Sheets with service-account JSON in the database dialog,
share private spreadsheets with the service account's `client_email`.
Both catalog validation and queries use the service account, not the Google
identity of the administrator adding the spreadsheet. This applies when creating
a connection and when editing its catalog.
The dialog's stored `impersonate_user` flag does not enable domain-wide delegation
for this service-account credential path. A spreadsheet shared only with the
administrator cannot be validated on that basis, even if the service account has
domain-wide delegation enabled. Creating an empty connection and editing it later
does not change the identity used to access the spreadsheet.
## Upload values
Date and datetime upload values are sent as ISO strings rather than numeric
nanosecond counts. Timezone-aware datetimes are converted to UTC, then uploaded
without a timezone suffix so Google Sheets can parse them as datetime cells
with `USER_ENTERED` input. For example, `2024-03-01 00:30:00+02:00` is uploaded as
`2024-02-29 22:30:00`. The original timezone is not retained; interpret these cell
values as UTC. Naive datetimes keep their original clock time.
Nullable integer columns retain integer values in the upload payload, and missing
values become empty cells. Time-only values use `HH:MM:SS[.ffffff]` strings.
Durations are sent as signed `H:MM:SS[.ffffff]` strings, with hours including days.

View File

@@ -112,6 +112,38 @@ When importing a dashboard ZIP with the **overwrite** option enabled, any existi
If you import without the overwrite flag, existing charts with conflicting UUIDs are left unchanged and the import skips those objects. Use overwrite when you want to push a fully updated dashboard (including chart definitions) from a development or staging environment to production.
## Charts and dashboards using semantic views
Semantic charts and dashboard filter/display-control targets can be restored against
an **existing semantic view with the same UUID**. The destination must enable
`SEMANTIC_LAYERS`, register the view's provider, and grant the importing user access
to the view or its parent semantic layer, in addition to the usual asset import
permissions. Missing, inaccessible or disabled dependencies fail the import rather
than producing a partially imported bundle.
Chart bundles identify these sources with `datasource_ref: {type: semantic_view,
uuid: <view-uuid>}`. Dashboard targets use `datasourceRef` with the same shape.
Import resolves the UUID and rewrites local datasource IDs; it never substitutes a
table with the same numeric ID. Ordinary table bundles retain `dataset_uuid`.
Older Superset readers that do not support semantic references cannot import these
semantic bundles.
Older dashboard bundles whose semantic filter targets lack `datasourceRef` are
rejected rather than binding their local IDs to unrelated tables. Re-export those
dashboards with a version that supports typed references. If a chart or dashboard
filter target references a deleted semantic view, export omits that reference so
other assets in the archive can still be exported. Repair the orphaned datasource
before exporting it for import elsewhere. Checks for inaccessible, disabled, and
unregistered existing semantic dependencies remain enforced, including in full
asset exports. Disabling `SEMANTIC_LAYERS` or removing a provider registration
therefore still blocks exports containing references to existing semantic views.
This is **reference restoration, not semantic-layer provisioning**. Bundles do not
include semantic-layer/view configuration or credentials, and importing does not
create provider connections or views, or contact providers. Provision the matching
view and access grants separately before importing. Full cross-workspace semantic
provisioning is outside this workflow.
## UUIDs in API Responses
The REST API POST endpoints for **datasets**, **charts**, and **dashboards** include the auto-generated `uuid` field in the response body:

View File

@@ -41,10 +41,139 @@ flowchart LR
C --> D[("Database<br/>(Postgres)")]
```
## Tool deadlines and concurrency
Protected synchronous and asynchronous MCP tools run in isolated thread workers,
each with its own Flask app
context and metadata database session. Warehouse I/O does not run on the
transport event loop. This includes SQL execution, chart data and previews,
chart compilation, dataset queries, and tools that discover warehouse metadata.
`MCP_TOOL_WORKERS` sets the maximum admitted warehouse-capable calls per
application/process. A full pool immediately returns an **MCP server busy**
error; it does not queue requests behind slow queries.
Tools that only read Superset's metadata database (the `list_*` and
`get_*_info` tools, `get_schema`, `get_instance_info`, `health_check`, and
similar) are admitted under a separate bound, `MCP_METADATA_TOOL_WORKERS`, so
they keep answering while slow warehouse queries fill `MCP_TOOL_WORKERS`. A
metadata-only tool that does reach a warehouse must first take a
`MCP_TOOL_WORKERS` slot, and reports **MCP server busy** if none is free.
Setting `MCP_METADATA_TOOL_WORKERS = 0` admits every tool under
`MCP_TOOL_WORKERS`.
Both bounds come out of one budget: the metadata pool's capacity,
`pool_size + max_overflow`. A warehouse-capable call can hold a metadata
connection for the whole of its warehouse query and cancelling it needs
another; a metadata-only call holds at most one; and the server's own
metadata I/O (tool-list filtering, audit writes, error hooks) runs on one
bounded thread, and API-key lookups on another of their own, so requests
with unknown keys cannot delay audit writes or RBAC filtering. Admission keeps
`2 * MCP_TOOL_WORKERS + MCP_METADATA_TOOL_WORKERS + 2` within the capacity,
so even with every slot in use no metadata connection checkout waits for
another holder. By default (`None`) warehouse-capable calls get about a third
of the connections left after those two threads, and metadata-only calls
what remains, each up to 16. With SQLAlchemy's default pool (5 + 10
connections) that is 4 warehouse-capable and 5 metadata-only calls. To admit
more, raise the pool for the MCP process, for example
`SQLALCHEMY_ENGINE_OPTIONS = {"pool_size": 20, "max_overflow": 20}` (12 and
14). Explicit values above the budget are reduced with a startup warning, and
a pool with fewer than 4 connections is rejected at startup. The server never
waits for a metadata connection on its event loop.
Pool logs report initialization and connection-budget clamping without logging
configuration values or engine-derived counts. Successful transport-layer API-key
validation logs the user id rather than the username or API key.
Tool-list and search permission checks, success/error audit writes, response-size
audits, and error hooks run in metadata threads with independent Flask contexts
and scoped sessions. These sessions are cleaned up when the operation finishes,
including after failure or client disconnection; inherited request contexts do
not share their ORM user/session with the thread.
`execute_sql` uses its request's `timeout` (default: 30 seconds). Tools without
a timeout parameter use `SQLLAB_TIMEOUT`. The deadline covers the complete tool
call, including composed queries, and returns a clear timeout error with a call
ID. Superset requests warehouse cancellation using its engine cancellation
hooks, including for connection prequeries, on a separate bounded pool.
Cancellation connections run independently of the expired tool deadline, including
connection prequeries such as PostgreSQL schema selection. Trino's client only
learns a query's `queryId` once it is running and then blocks until results
arrive, so the cancellation thread reads that id from the live cursor and kills
the query with `system.runtime.kill_query`. Engines whose specs expose no
cancellation handle (for example BigQuery, Databricks native and Athena) are not
cancelled. Drivers without cancellation support can
continue running after the caller receives the timeout; the worker retains its
slot and owns session cleanup until it finishes, while metadata-only tools keep
their own bound. A stuck cancellation also
retains its call's slot rather than exhausting cancellation capacity for new
calls. One warning with the call ID records abandonment. A timeout does not guarantee that warehouse writes were
rolled back, so do not blindly retry mutating SQL.
## Native filter defaults
`manage_native_filters` accepts `default_value` for select filters. An empty
list clears the selection. With `enable_empty_filter: true`, that explicit
empty default matches no rows in both the dashboard UI and server-side
dashboard chart queries, including for UI-created non-exact select filters.
Creating a required select filter without `default_value` also stores this
match-nothing default, unless `default_to_first_item` is enabled.
Inverse-selection filters retain the UI's no-predicate behavior for empty
selections. Disabling `enable_empty_filter` removes the empty default's
match-nothing predicate without requiring another `default_value` update.
Explicit defaults, including an empty list, cannot be combined with
`default_to_first_item: true`. Creating a first-item filter or enabling that
option on an existing filter leaves the stored selection unset so the dashboard
can choose the first available item after loading options. Toggling
`enable_empty_filter` on a first-item filter preserves that behavior rather than
storing an explicit empty selection or match-nothing predicate. Any update to
a first-item filter also clears a defined legacy stored selection (including
`null`) when first-item mode remains enabled, allowing automatic selection.
Select filters with legacy string-column
targets support both default updates and applied selections.
## Bounding dashboard dataset metadata
`get_dashboard_datasets` lists a dashboard's datasets and semantic views with
column and metric details. Its optional `max_columns` and `max_metrics`
parameters limit the number of columns (0–100, default 100) and metrics (0–50,
default 50) returned **per dataset**. Columns and metrics are sorted by name
before the caps apply, so repeated calls return the same subset. For a wide
dashboard, start with smaller caps or omit the details entirely:
```json
{
"request": {
"identifier": 123,
"max_columns": 0,
"max_metrics": 0
}
}
```
Dataset identities, chart counts, and total column and metric counts are
retained. `columns_truncated` and `metrics_truncated` are true whenever fewer
items are returned than the dataset contains, including when a cap of 0 omits a
nonempty list. Use `get_dataset_info` to fetch details for an individual
dataset. Omitting both parameters returns up to 100 columns and 50 metrics per
dataset. The caps bound per-dataset details, not the number of datasets or the
entire response's byte size; a dashboard with many datasets can still exceed the
server's configured byte limit.
## Dashboard filter notifications
`apply_dashboard_filters` supports exact-match select filters without inverse
selection, and time filters. Inverse-selection configurations are rejected.
selection, time filters, numeric range filters, and time-grain filters. Range
bounds must be finite numbers representable as floats, or null; booleans are
rejected. Range permalink labels use the dashboard's smart number formatting
with ASCII minus signs, falling back to plain numeric representations if
formatting overflows; time-grain labels come from
the target datasource's grain names when available. Time grains must be supported
by the filter's target dataset's database engine, as listed in the datasource's
`time_grain_sqla` options. If the filter has a nonempty configured allowlist, the
selected grain must also appear in that list. An unresolved target dataset is
rejected.
Inverse-selection configurations are rejected.
Select filters configured for contains, starts-with, or ends-with matching are
rejected rather than silently applying exact matching. A single-select filter
accepts at most one value; a longer list is rejected rather than stored as a
@@ -96,11 +225,13 @@ The MCP process and websocket transport must use the same
Framework feature flag.
The response's `live_update_pushed` field reports publication success, not
browser receipt or application. Without a backend, an identifiable principal,
or a successful publish, it is `false`; the created `dashboard_url` remains
the fallback. An open dashboard resolves matching notifications through the
existing dashboard permalink API and applies the returned filters without a
reload. A “Filters applied from chat” toast offers **Undo**, restoring the
browser receipt or application. When `WEBSOCKET_ENABLE` is disabled or unset,
no notification is published and this field is `false`. Without a backend,
`can_read` on `Realtime` for the calling principal, an identifiable principal,
or a successful publish, it is also `false`; the created
`dashboard_url` remains the fallback. An open dashboard resolves matching
notifications through the existing dashboard permalink API and applies the
returned filters without a reload. A “Filters applied from chat” toast offers **Undo**, restoring the
affected filters' previous selections. The toast carries an action, so it has
no auto-dismiss timer and stays up until Undo or its close button is used,
which keeps the action reachable for keyboard and assistive-technology users.
@@ -114,6 +245,77 @@ latest-per-user permalink lookup, and replaying a previously received permalink
could overwrite manual changes or undo. If a notification is missed or its
permalink cannot be resolved, open `dashboard_url` from chat.
## Dashboard filter scope
A client that answers questions about a dashboard the user is viewing (for
example an assistant embedded next to it) can make every tool call honor that
dashboard's active filters by sending them in the `X-Superset-Dashboard-Scope`
request header on each MCP HTTP request of the conversation turn. The model
issuing tool calls never sees the header, so it cannot drop or loosen the
filters, and retried calls carry them again.
The header value is `base64url(zlib(JSON))` of:
```json
{
"version": 1,
"dashboard_id": 12,
"chart_filters": {
"101": {"filters": [{"col": "client", "op": "IN", "val": ["A"]}],
"time_range": "Last quarter"}
}
}
```
`chart_filters` maps each chart with active filters to the `extra_form_data`
the dashboard applies to it. A malformed header refuses every tool call rather
than being ignored. Without the header, behavior is unchanged.
While a scope with active filters is present:
- Chart data tools (`get_chart_data`, `get_chart_preview`, `get_dashboard_data`)
apply each chart's dashboard filters and AND any `extra_form_data` the model
supplies with them. Filters can be added but not removed, and time windows,
time columns and relative anchors the dashboard sets (including time-series
cross-filters) cannot be changed. Charts that are not on the scoped
dashboard, and unsaved chart state (`form_data_key`), are refused.
- `query_dataset`, `get_table` (datasets) and `execute_sql` apply the
filters from dashboard charts backed by the queried dataset, respecting
native-filter chart scoping. Datasets outside the dashboard use the combined
dashboard-wide filters. A model-supplied time range is intersected with the
applicable dashboard time range. `execute_sql` replaces each filtered table
read with a sub-query, the same way row-level security is applied, and refuses
SQL whose tables are not registered datasets that have every filtered column, as well
as SQL that could read tables it cannot see: opaque statements, functions
the SQL parser does not model, and table functions. For `execute_sql`, template
syntax in SQL or dashboard filter values is refused rather than rendered after
scoping.
For all three tools, the time-filter column is resolved from the affected
dashboard charts' temporal filters (or legacy chart time-column settings),
after dashboard overrides. The charts must agree on one column. A chart with
no temporal target, conflicting targets, or a dataset outside the dashboard
with an applicable time window causes a refusal; the dataset's main datetime
column and a model-supplied column are never used to guess the dashboard target.
Malformed saved chart filter lists also cause an explicit scope refusal before
dashboard overrides are merged; no query is run.
Without an applicable dashboard time window, no chart temporal target is
required, and model-supplied time filtering behaves as before.
- Writes that change what charts or datasets query (adding a chart to a
dashboard, saving a chart, defining a virtual dataset, editing a dataset or
metric, restoring a chart or dataset) are refused while the scope is
active.
- Where the filters cannot be applied (for example, a column the dataset lacks,
or a column filtered differently by different charts) the call fails with a
tool error beginning `Dashboard filter scope refused this call:`. Tools that
return no dataset rows are unaffected; any other tool, including extension
tools, is refused.
- MCP response caching is bypassed for scoped calls, and the scope is applied
before any query is built, so Superset's query cache never serves an
unscoped result.
The scope is a correctness control for answers, not an access control.
Permissions and row-level security apply exactly as they do without it.
---
## Quick Start
@@ -776,12 +978,40 @@ MCP_TOOL_SEARCH_CONFIG = {
| `always_visible` | See above | Tools that always appear in `list_tools`, regardless of search |
| `include_schemas` | `True` | Include the full `inputSchema` in search results by default. When `False` ("summary mode"), search results omit `inputSchema` entirely and include a lightweight `parameters_hint` listing top-level parameter names. Full schemas are always used when a tool is actually invoked via `call_tool`. |
| `compact_schemas` | `True` | Legacy setting selecting the default description limit (300 when `True`, 0 when `False`) if `max_description_length` is omitted. Input schemas preserve `$defs`, `$ref`, nullable unions, and constraints in either mode; titles are omitted. Clients should resolve references within each tool's `inputSchema`. |
| `max_description_length` | `300` | Truncate tool descriptions in search results (0 = no truncation). Applies in both summary and full-schema modes. |
| `max_description_length` | `300` | Budget for discovery prose and request-wrapper instructions (0 = no prose truncation). Complete paragraphs are retained, followed by complete sentences of the next paragraph unless it is a list, heading, or IMPORTANT block. Applies in both summary and full-schema modes. |
The no-query catalog uses the same bounded serializer as targeted search.
Chart creation/update and virtual-dataset tools keep request wrapping, save/preview
behavior, required URL display, and workflow guidance in request metadata rather
than relying on truncated introductory prose. Paragraph truncation never leaves a
partial IMPORTANT block or numbered workflow in the catalog.
Calling constraints are authored as a `Field(description=...)` on the tool's
`request` parameter and served as the `request` property's schema description, so
schema-first clients can identify the wrapper without remembering previous calls.
Summary mode preserves these instructions in `parameters_hint`. Instructions are
never truncated: their length is deducted from the prose budget first. A request
model's own docstring is not a calling instruction and is never deducted. If a
configured limit is smaller than the instructions, prose is omitted rather than
cutting the instructions. Built-in calling instructions are capped at 300
characters by regression tests, and the complete tool entries retain per-tool
size budgets. Field-level descriptions in the default full input schema also
preserve tool-specific search semantics, sortable columns, chart pagination, and
layout guidance. These details are not included in schema-free summary mode.
This setting does not change result-size limits or schema validation.
:::tip
Set `enabled: False` to revert to the traditional "show all tools at once" behavior, which some clients or workflows may prefer.
:::
With either the `bm25` or `regex` strategy, a query matching a tool name is
promoted ahead of other matches before the `max_results` limit is applied. Matching
ignores case and treats underscores and whitespace as equivalent separators
(including leading, trailing, or repeated whitespace). Only tools visible to
the caller are eligible. Pinned `always_visible` tools remain in `list_tools`
rather than search results; non-exact queries retain BM25 ranking or regex
catalog ordering.
Tool search reduces the initial token cost from ~15–20K tokens (full catalog) down to ~4–5K tokens (pinned tools + search interface) — roughly 85% savings at the start of each conversation.
### Session & CSRF
@@ -819,6 +1049,7 @@ Each tool declares one or more required FAB permissions. The table below maps to
| `list_dashboards`, `get_dashboard_info`, `generate_dashboard`, `add_chart_to_existing_dashboard` | `can_read` on `Dashboard` (read), `can_write` on `Dashboard` (mutate) |
| `get_dashboard_data` | `can_read` on `Dashboard` **and** `can_read` on `Chart` (it reads chart data across the dashboard) |
| `list_datasets`, `get_dataset_info`, `create_virtual_dataset` | `can_read` on `Dataset` (read), `can_write` on `Dataset` (mutate) |
| `list_metrics`, `get_table`, `get_compatible_dimensions`, `get_compatible_metrics` | `can_read` on `Dataset`, plus a data-model metadata permission (`can_get_drill_info`, `can_get_or_create_dataset`, or `can_write` on `Dataset`); external semantic views also enforce their own datasource access check |
| `list_databases`, `get_database_info` | `can_read` on `Database` |
| `get_catalog` | `can_read` on the requested asset class (`Database`, `Dataset`, `Chart`, or `Dashboard`); databases and datasets also require data-model metadata access, otherwise the page is marked `restricted` |
| `execute_sql` | `can_execute_sql_query` on `SQLLab` |
@@ -1047,6 +1278,23 @@ while page["next_cursor"]:
)
```
## Chart datasource metadata
For roles allowed to view data-model metadata, `get_chart_info` includes
`datasource_id` in its default columns. Dashboard chart summaries include
`datasource_type` alongside `datasource_id`, which is only unique within its type.
Both fields are redacted for users without data-model metadata access.
Chart and dashboard chart summaries resolve dataset names from the live table
or semantic view, so renames are reflected without resaving the chart. Query and
saved-query charts retain their stored datasource names; missing tables or
semantic views do not fall back to stale names. Dataset-access validation remains
unchanged.
`list_charts` filters on `datasource_name` use these same live names before
pagination and counting. For tables with a schema, filter using
`schema.table_name`. Data-model metadata permission checks also apply to these
filters and returned identifiers.
## Trash Listing (Soft-Deleted Objects)
When the `SOFT_DELETE` feature flag is enabled, deleting a chart or dashboard moves it to trash rather than removing it outright. The `list_charts` and `list_dashboards` tools accept a `deleted_state` parameter to surface those trashed objects:
@@ -1191,6 +1439,36 @@ filters update the `BASE_AXIS` column's `timeGrain`, regardless of its position
in the query's `columns` list. Other columns are left untouched. Queries without
a `BASE_AXIS` column continue to use `extras.time_grain_sqla`.
### Exploring large dashboard layouts
`get_dashboard_layout` returns the full parsed layout by default. For large
dashboards, pass `tabs_only=true` to retrieve just the tab tree in `tab_tree`:
`id`, `name` (the title), `parent_tab_id`, zero-based `depth`, and
`chart_count` (distinct charts in the tab and its descendants). In this mode
`tabs` and `charts` are empty by request, not because the dashboard is empty.
Then pass `tab="<ID or exact title>"` to retrieve that tab and its descendants
with chart positions. Matching is case-sensitive; IDs take precedence and
ambiguous titles require an ID. Both options can be combined to summarize a
subtree. Parent IDs, depths, and chart paths retain their full-dashboard context.
Charts placed outside every tab are counted in `untabbed_chart_count` in every
response; pass `untabbed_only=true` (without `tab` or `tabs_only`) to list only
those charts. The `scope` field echoes the applied `tabs_only`, resolved
`tab_id`, and `untabbed_only` values, and is `null` for the full layout.
Scoping only removes tabs and chart placements from the layout the caller can
already retrieve. Conflicting scope options return an actionable
`invalid_scope` error naming the options to remove; blank tab selectors return
`tab_not_found` rather than widening the scope. On dashboards without tabs,
`tabs_only=true` returns `tab_not_found` with guidance to use
`untabbed_only=true` or request the full layout instead.
Permalink active-tab and filter state remain unchanged; only an explicit `tab`
scopes the response. Oversized layout errors suggest the next narrower scope:
the tab tree and untabbed charts for a full tabbed layout, a top-level tab for
an oversized tab tree, a nested tab for a tab that has one, and `list_charts`
paging when a dashboard without tabs or a tab without nested tabs is still too
large.
### Reading applied dashboard filters
When answering about a filtered dashboard, call `get_dashboard_info` with the
@@ -1240,6 +1518,57 @@ sub-500 exception remains a warning even if it carries a connection error reason
the generic engine reason can also describe a caller's malformed column, and a
connection access denial is still a permission failure.
The error hook runs off the transport loop in its own application and metadata
session scope, without reloading the request user. Reporting an error therefore
does not require metadata database access unless the hook itself uses it. Use the
provided hook context for caller identity rather than `g.user`.
## SQL execution row limits
The `execute_sql` tool treats an explicit request `limit` as a safety cap on the
last statement's outer SQL LIMIT. The smaller limit wins: SQL `LIMIT 5` with
request `limit: 10` returns at most five rows, while SQL `LIMIT 10` with request
`limit: 5` also returns at most five. Equal limits and SQL `LIMIT 0` are preserved.
If SQL has no outer limit, the explicit request cap is applied. Inner query and
CTE limits and SQL offsets are preserved. Literal `FETCH FIRST` counts and
parenthesized literal limits follow the same rule. `PERCENT`, `WITH TIES`, and
limit expressions that cannot be interpreted as a fixed row count are preserved
inside a subquery with an outer row cap, rather than replaced. This conservative
fallback cannot increase the original result's row count. For SQL Server, these
restrictions are left unchanged instead: a derived table could reject duplicate
or unnamed output columns. The executor caps the last statement's returned rows
at the smaller of the explicit request limit and `SQL_MAX_ROW`, without changing
percentage, tie, or expression semantics. Cursor reads are bounded by the same
cap plus one probe row, including across multiple fetch batches, preserving
engine-specific result processing. Only successfully returned rows count against the cursor
budget, so a failed fetch leaves that budget available for an engine fallback.
BigQuery only falls back if its initial fetch fails; errors after a successful
sample, including during the EOF probe, fail the query rather than silently
discarding buffered rows.
This does not bound the work performed by the database.
Because this cap applies to any row-returning last statement, a non-query
statement such as `SHOW TABLES` or DML with `RETURNING` can also be capped. Each
statement reports `truncated: true` when a fetch cap omits rows from the
executed SQL result, including engine memory caps such as `BQ_FETCH_MAX_MB`.
Exactly-at-cap and smaller results report `false` when no rows are omitted. This
flag does not indicate rows excluded by a SQL LIMIT visible in `executed_sql`,
and does not bound or count affected rows for a write with `RETURNING`. In
`dry_run`, the SQL Server restriction remains unchanged because no results are
fetched.
Omitting `limit` leaves the SQL limit unchanged. Explicit request caps remain
subject to the server's `SQL_MAX_ROW` configuration. This behavior also applies
to `dry_run`, which returns the transformed SQL without executing it.
The SQL-only `Database.apply_limit_to_sql()` helper used by dataset column
discovery and alerts does not use the executor's bounded cursor. It retains the
legacy SQL Server behavior of replacing `PERCENT`, `WITH TIES`, and non-fixed
limit expressions with a fixed `TOP` cap, without introducing derived tables
that could reject unnamed or duplicate columns. Smaller fixed limits, including
zero, are preserved unless the caller explicitly requests `force=True`. This
legacy helper's replacement semantics are separate from MCP execution's
preservation of complex SQL restrictions.
## Result serialization
Data-bearing tool responses sanitize row values, column samples, and statistics
@@ -1255,6 +1584,39 @@ For saved and unsaved charts, `get_chart_data` column metadata excludes missing
values from `unique_count`. Values serialized as JSON `null`, including NaN,
NaT, and non-finite numbers, contribute to `null_count` instead.
## Big Number headlines
For Big Number charts (`big_number` and `big_number_total`), `get_chart_data`
and `get_dashboard_data` return a `headline` object holding the number the chart
displays. Agents should report `headline.value` rather than reading or summing
the data rows: for the trendline variant the rows are a time series, and the
displayed number is derived from the whole series by the chart's aggregation.
| Field | Contents |
| --- | --- |
| `value` | The displayed number, or `null` when it cannot be computed exactly |
| `aggregation` | `total` for `big_number_total`; otherwise the chart's aggregation (`LAST_VALUE`, `sum`, `mean`, `min`, `max`, `median`, or `raw`) |
| `rows_used` | The number of non-null values aggregated |
| `reason` | Why `value` is `null` |
A headline is only returned when it matches the rendered chart, so `value` is
`null` with a `reason` when:
- The query returned no rows, the metric column is missing, or every metric
value is null.
- The chart applies a rolling window or resampling that the executed query did
not include, or the fetch was truncated at the row limit.
- The aggregation picks the latest value (`LAST_VALUE`) and some rows have no
usable timestamp. The chart's ordering of such rows depends on row order, so
the value it shows cannot be determined exactly. Order-independent
aggregations still include those rows.
- The aggregation is `raw` ("Overall value") and the chart has no saved query
context. That value comes from a second query that only a saved query context
carries, so unsaved and `form_data_key` requests return `null`.
When `form_data_key` is supplied, the headline follows the unsaved visualization
type, including when switching to or from a Big Number chart.
## Dataset query dimensions and nested fields
The `query_dataset` tool accepts exact column names registered on the selected
@@ -1410,3 +1772,53 @@ predicate, whose behavior varied by database. It is not an indexed
full-text search: large catalogs and long descriptions can increase scan time.
Benchmark with your metadata database and catalog size when tuning discovery
latency.
### Managing and applying dashboard native filters
`manage_native_filters` requires a numeric dataset column when adding a
`filter_range` filter or changing its dataset/column target. Select filters
continue to accept nonnumeric columns.
For `apply_dashboard_filters`, pass `time_grain` as a list containing at most
one datasource-supported duration (for example, `["P1D"]`). Custom engine
durations are accepted when exposed in the datasource's time-grain options,
rather than limited to a fixed enum.
A nonempty `time_grains` list in the saved filter configuration restricts the
allowed selection to grains that appear in both that list and the target dataset's
supported time grains; missing or empty lists use the dataset's supported grains.
Pass `[]` to clear an optional time-grain filter; required filters cannot be
cleared.
## Missing chart columns
`generate_chart`, `update_chart`, `update_chart_preview` and
`generate_explore_link` all validate column references through
`validate_and_compile` and return the same structured errors.
A reference to a missing column returns `column_not_found` with up to three
similar physical column names ("Did you mean: ..."), or explicit no-match
guidance when nothing is close enough. Saved metrics — both names and
expressions — are excluded from these candidates for dimension and `WHERE`
references, because only a physical column is legal there and suggesting a
metric leads to invalid `SUM(metric)` SQL. A `HAVING` filter subject may
legitimately name a saved metric, so metrics are included for that slot. When a
reference in a metric slot (`y`, `metric`, or an aggregated table column, for
example) near-misses a saved metric name, the error adds a hint to reference it
as `{"name": "<metric>", "saved_metric": true}` instead. Dimension references
never receive this hint, even when they carry an `aggregate`.
When more than one distinct column is missing, the error type is
`multiple_invalid_columns`. The invalid names appear in escaped, length-limited
details, and the three-candidate cap is **shared across all missing columns**,
filled round-robin so each missing column gets a turn.
Column errors carry a bounded `dataset_context`: up to ten column names and ten
saved-metric names from the authorized dataset, names only — never SQL
expressions. Suggested candidates are listed first so they are never cut by the
bound (a hinted saved metric leads `available_metrics` the same way), and the
error says `Showing 10 of N columns` or `Showing 10 of N saved metrics` when a
list is partial.
Context names are returned verbatim per the Tool Result Value Contract.
"Did you mean" column candidates also preserve dataset spelling, including
quotes and ampersands, with a 200-character bound per candidate. Use
`get_dataset_info` for the complete schema.

View File

@@ -201,7 +201,6 @@ came out of, credit to the community for working it out.
`docker-compose` build to take much less time and resources
- `INCLUDE_CHROMIUM`: whether to include chromium in the backend build so that it can be
used as a headless browser for workloads related to "Alerts & Reports" and thumbnail generation
- `INCLUDE_FIREFOX`: same as above, but for firefox
- `PY_VER`: specifying the base image for the python backend, we don't recommend altering
this setting if you're not working on forwards or backwards compatibility

View File

@@ -200,6 +200,11 @@ when authorization is denied or callback state is missing or invalid. Provider
error text is not echoed in the response; authorization denials use the message
"The OAuth2 provider denied the request". Restart the OAuth2 flow to retry.
When a database rejects an OAuth2 access token, connection and metadata
inspection attempt one forced refresh and retry. Reopening a connection inside
that query retry does not trigger another exchange. If the replacement token is
also rejected, Superset asks the user to sign in again.
#### Data Exploration
<details>

View File

@@ -95,8 +95,7 @@ documentation.
Affecting the Docker build process:
- **SUPERSET_BUILD_TARGET (default=dev):** which --target to build, either `lean` or `dev` are commonly used
- **INCLUDE_FIREFOX (default=false):** whether to include the Firefox headless browser in the build
- **INCLUDE_CHROMIUM (default=false):** whether to include the Chromium headless browser in the build
- **INCLUDE_CHROMIUM (default=false):** whether to include Playwright and the Chromium headless browser in the build, which Alerts & Reports and thumbnails need to take screenshots
- **BUILD_TRANSLATIONS(default=false):** whether to compile the translations from the .po files available
- **SUPERSET_LOAD_EXAMPLES (default=yes):** whether to load the examples into the database upon startup,
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`. Once the example
@@ -579,7 +578,7 @@ There are three types of assets you can build:
1. `npm run build`: the production assets, CSS/JSS minified and optimized
2. `npm run dev-server`: local development assets, with sourcemaps and hot refresh support
3. `npm run build-instrumented`: instrumented application code for collecting code coverage from Cypress tests
3. `npm run build-instrumented`: instrumented application code for collecting code coverage from end-to-end tests
If while using the above commands you encounter an error related to the limit of file watchers:
@@ -722,7 +721,7 @@ can find all of the workflows and other assets under the `.github/` folder. This
- running the backend unit test suites (`tests/`)
- running the frontend test suites (`superset-frontend/src/**.*.test.*`)
- running our Playwright end-to-end tests (`superset-frontend/playwright/`) and legacy Cypress tests (`superset-frontend/cypress-base/`)
- running our Playwright end-to-end tests (`superset-frontend/playwright/`)
- linting the codebase, including all Python, Typescript and Javascript, yaml and beyond
- checking for all sorts of other rules conventions
@@ -797,7 +796,7 @@ pytest tests/unit_tests/*
#### Integration Tests
For more complex pytest-defined integration tests (not to be confused with our end-to-end Cypress tests), many tests will require having a working test environment. Some tests require a database, Celery, and potentially other services or libraries installed.
For more complex pytest-defined integration tests (not to be confused with our end-to-end Playwright tests), many tests will require having a working test environment. Some tests require a database, Celery, and potentially other services or libraries installed.
### Running Tests with `act`
@@ -908,8 +907,6 @@ superset:
user: "root"
depends_on: *superset-depends-on
volumes: *superset-volumes
environment:
CYPRESS_CONFIG: "${CYPRESS_CONFIG}"
```
Start Superset as usual

View File

@@ -107,6 +107,17 @@ npm link superset-plugin-chart-hello-world
7. **Import and register in Superset**:
Edit `superset-frontend/src/visualizations/presets/MainPreset.ts` to include your plugin.
### Sharing colors in composed charts
`ChartProps.colorScale` accepts an optional `CategoricalColorScale`. A parent
visualization can pass the same scale to its child chart transforms so categories
use consistent colors across the composition. Create a fresh scale for each parent
transformation using the selected color scheme. Child plugins must explicitly use
the supplied scale, falling back to their own scale when it is absent.
Cartodiagram uses this contract with Pie charts to coordinate category colors
across map locations in Explore.
## Testing
### Python Testing
@@ -166,11 +177,9 @@ npm run test -- MyComponent.test.tsx
### E2E Integration Testing
We support both Playwright (recommended) and Cypress for end-to-end testing.
We use Playwright for end-to-end testing.
#### Playwright (Recommended - NEW)
Playwright is our new E2E testing framework, gradually replacing Cypress.
#### Playwright
```bash
# Navigate to frontend directory
@@ -199,30 +208,6 @@ npm run playwright:debug tests/auth/login.spec.ts
npm run playwright:report
```
#### Cypress (DEPRECATED - will be removed)
Cypress is being phased out in favor of Playwright but is still available:
```bash
# Set base URL for Cypress
export CYPRESS_BASE_URL='http://localhost:8088'
export CYPRESS_DATABASE=test
export CYPRESS_USERNAME=admin
export CYPRESS_PASSWORD=admin
# Navigate to Cypress directory
cd superset-frontend/cypress-base
# Run interactively
npm run cypress-debug
# Run headless (like CI)
npm run cypress-run-chrome
# Run specific file
npm run cypress-run-chrome -- --spec "cypress/e2e/explore/chart.test.js"
```
### Debugging Server App
For debugging the Flask backend:

View File

@@ -36,7 +36,7 @@ This is a list of statements that describe how we do frontend development in Sup
- We use [@emotion](https://emotion.sh/docs/introduction) to provide styling for our components, co-locating styling within component files.
- See: [SIP-37](https://github.com/apache/superset/issues/9145)
- See: [Emotion Styling Guidelines and Best Practices](./frontend/emotion-styling-guidelines.md)
- We use Jest for unit tests, React Testing Library for component tests, and Cypress for end-to-end tests.
- We use Jest for unit tests, React Testing Library for component tests, and Playwright for end-to-end tests.
- See: [SIP-56](https://github.com/apache/superset/issues/11830)
- See: [Testing Guidelines and Best Practices](../testing/testing-guidelines.md)
- We add tests for every new component or file added to the frontend.

View File

@@ -71,6 +71,7 @@ The Alerts & Reports feature relies on Celery for task scheduling and execution.
- Redis running on `localhost:6379`
- [MailHog](https://github.com/mailhog/MailHog) installed (a local SMTP server with a web UI for viewing caught emails)
- Playwright and Chromium installed, for report screenshots
### superset_config.py
@@ -124,8 +125,6 @@ ALERT_REPORTS_EXECUTORS = [ExecutorType.EDITOR]
FEATURE_FLAGS = {
"ALERT_REPORTS": True,
# Recommended for better screenshot support (WebGL/DeckGL charts)
"PLAYWRIGHT_REPORTS_AND_THUMBNAILS": True,
}
```

View File

@@ -24,12 +24,10 @@ under the License.
# End-to-End Testing
Apache Superset uses Playwright for end-to-end testing, migrating from the legacy Cypress tests.
Apache Superset uses Playwright for end-to-end testing.
## Running Tests
### Playwright (Recommended)
```bash
cd superset-frontend
@@ -53,16 +51,6 @@ npm run playwright:debug tests/auth/login.spec.ts
# or: npx playwright test --debug tests/auth/login.spec.ts
```
### Cypress (Deprecated)
Cypress tests are being migrated to Playwright. For legacy tests:
```bash
cd superset-frontend/cypress-base
npm run cypress-run-chrome # Headless
npm run cypress-debug # Interactive UI
```
## Project Architecture
```
@@ -179,10 +167,10 @@ All debugging artifacts are available in the HTML report for easy analysis.
## Configuration
- **Config**: `playwright.config.ts` - matches Cypress settings
- **Config**: `playwright.config.ts`
- **Base URL**: `http://localhost:8088` (assumes Superset running)
- **Browsers**: Chrome only for Phase 1 (YAGNI)
- **Retries**: 2 in CI, 0 locally (matches Cypress)
- **Retries**: 2 in CI, 0 locally
## Contributing Guidelines
@@ -207,16 +195,6 @@ All debugging artifacts are available in the HTML report for easy analysis.
3. **Actions and queries only** - no assertions
4. **Use existing components** for DOM interactions
## Migration from Cypress
When porting Cypress tests:
1. **Port the logic**, not the implementation
2. **Use page objects** instead of inline selectors
3. **Replace `cy.intercept/cy.wait`** with `page.waitForRequest()`
4. **Use shared constants** from `utils/urls.ts`
5. **Follow the established patterns** shown in `tests/auth/login.spec.ts`
## Best Practices
- **Centralize selectors** in page objects

View File

@@ -169,7 +169,7 @@ npm run test:coverage
- **Jest** - Unit and integration testing framework
- **React Testing Library** - Component testing utilities
- **Playwright** - End-to-end testing (replacing Cypress)
- **Playwright** - End-to-end testing
- **Storybook** - Component development and testing
---

View File

@@ -26,7 +26,7 @@ under the License.
We feel that tests are an important part of a feature and not an additional or optional effort. That's why we colocate test files with functionality and sometimes write tests upfront to help validate requirements and shape the API of our components. Every new component or file added should have an associated test file with the `.test` extension.
We use Jest, React Testing Library (RTL), and Cypress to write our unit, integration, and end-to-end tests. For each type, we have a set of best practices/tips described below:
We use Jest, React Testing Library (RTL), and Playwright to write our unit, integration, and end-to-end tests. For each type, we have a set of best practices/tips described below:
## Jest
@@ -80,9 +80,9 @@ Prefer the [user-event](https://github.com/testing-library/user-event) library,
You can find an example of a test [here](https://github.com/apache/superset/blob/master/superset-frontend/src/dashboard/components/PublishedStatus/PublishedStatus.test.tsx).
## Cypress
## Playwright
### Prefer to use Cypress for e2e testing and RTL for integration testing
### Prefer to use Playwright for e2e testing and RTL for integration testing
Here it's important to make the distinction between e2e and integration testing. This [article](https://www.onpathtesting.com/blog/end-to-end-vs-integration-testing) gives an excellent definition:
@@ -91,30 +91,11 @@ Here it's important to make the distinction between e2e and integration testing.
> A typical software project consists of multiple software units, usually coded by different developers. Integration testing combines those software units logically and tests them as a group.
> Essentially, integration testing verifies whether or not the individual modules or services that make up your application work well together. The purpose of this level of testing is to expose defects in the interaction between these software modules when they are integrated.
Do not use Cypress when RTL can do it better and faster. Many of the Cypress tests that we have right now, fall into the integration testing category and can be ported to RTL which is much faster and gives more immediate feedback. Cypress should be used mainly for end-to-end testing, replicating the user experience, with positive and negative flows.
Do not use Playwright when RTL can do it better and faster. Tests that fall into the integration testing category belong in RTL, which is much faster and gives more immediate feedback. Playwright should be used mainly for end-to-end testing, replicating the user experience, with positive and negative flows. See [End-to-End Testing](./e2e-testing.md) for the Playwright setup and conventions.
### Isolated and standalone tests
Tests should never rely on other tests to pass. This might be hard when a single user is used for testing as data will be stored in the database. At every new test, we should reset the database.
### Cleaning state
Cleaning the state of the application, such as resetting the DB, or in general, any state that might affect consequent tests should always be done in the `beforeEach` hook and never in the `afterEach` one as the `beforeEach` is guaranteed to run, while the test might never reach the point to run the `afterEach` hook. One example would be if you refresh Cypress in the middle of the test. At this point, you will have built up a partial state in the database, and your clean-up function will never get called. You can read more about it [here](https://docs.cypress.io/guides/references/best-practices#Using-after-or-afterEach-hooks).
### Unnecessary use of `cy.wait`
- Unnecessary when using `cy.request()` as it will resolve when a response is received from the server
- Unnecessary when using `cy.visit()` as it resolves only when the page fires the load event
- Unnecessary when using `cy.get()`. When the selector should wait for a request to happen, aliases would come in handy:
```js
cy.intercept('GET', '/users', [{ name: 'Maggy' }, { name: 'Joan' }]).as(
'getUsers',
);
cy.get('#fetch').click();
cy.wait('@getUsers'); // <--- wait explicitly for this route to finish
cy.get('table tr').should('have.length', 2);
```
Tests should never rely on other tests to pass. Create the data a test needs through the API helpers and the `testAssets` fixture, which cleans up what it tracked, instead of depending on state left behind by another test.
### Accessibility and Resilience
@@ -127,5 +108,5 @@ The same accessibility principles in the RTL section apply here. Use accessible
- [Fix the "not wrapped in act(...)" warning](https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning)
- [Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)
- [ARIA Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
- [Cypress - Best Practices](https://docs.cypress.io/guides/references/best-practices)
- [Playwright - Best Practices](https://playwright.dev/docs/best-practices)
- [End-to-end vs integration tests: what's the difference?](https://www.onpathtesting.com/blog/end-to-end-vs-integration-testing)

View File

@@ -54,6 +54,9 @@ username: admin
password: admin
```
The personalized Home page requires you to sign in. Opening Home from the login
page or a public dashboard redirects you to the login page first.
#### 🎉 Congratulations! Superset is now up and running on your machine! 🎉
### Wrapping Up

View File

@@ -350,6 +350,24 @@ Only enable HTML rendering for columns sourced from data you control. Rendering
Column headers display a tooltip with the column's **Description** from the dataset editor when the user hovers over them. Keep dataset column descriptions up to date to improve chart discoverability.
#### Multi-level Header
The **Table** and **AG Grid Interactive Table** chart types can group column headers under shared parent labels. Open the **Customize** tab and find the **Multi-level header** section. Use **Column groups** to nest related columns — for example, several sales metrics under a **Sales** header.
To create a group:
1. Click **Add group**.
2. Enter a **Name**. A group cannot be saved without one.
3. Select one or more **Columns**. A column can belong to only one group. Percent metrics (`%metric`) stay independently groupable and are not reserved by the matching base metric.
4. Optionally set **Label position** (left, center, or right) and **Table side** (left or right) to control where the group header sits.
5. Click **Apply**.
After a group is saved, click its numbered label (**Group 1**, **Group 2**, …) to edit it. Drag the handle on a group to reorder groups. While editing, **Add subgroup** nests another header under the current group (up to three levels). Subgroups have the same name and column requirements as top-level groups.
When **Advanced Analytics** → **Time Comparison** is enabled on an **Aggregate** query with **Calculation type** set to **Values**, Superset automatically creates one group per metric (`Main`, `#`, `△`, `%`). Those groups use the metric's dataset label, lock their name and columns, and disappear when time comparison is turned off. You can still add separate manual groups for other columns, including percent metrics.
On the standard Table chart, **Allow columns to be rearranged** is hidden while header groups or time comparison are active, so body column order stays aligned with the grouped header.
#### Display Controls
In dashboard view mode (without entering Edit mode), charts with configurable display options expose a **Display Controls** panel accessible from the chart's context menu. This surfaces controls such as Time Grain, Time Column, and layer visibility for applicable chart types — making it easy to adjust a chart's view without going to Explore.
@@ -379,6 +397,8 @@ AG Grid supports server-side column filters that query the full dataset — not
AG Grid Interactive Table supports **Time Shift** (time comparison), matching the behavior of the standard Table chart. In the **Advanced Analytics** → **Time Comparison** section of the chart configuration, enter a shift expression (e.g., `1 year ago`, `minus 7 days`) to add comparison columns showing values from the offset period. Dashboard-level time range overrides apply to both the base and comparison periods.
When time comparison is active in **Aggregate** / **Values** mode, the **Customize** → **Multi-level header** section automatically groups each metric's comparison columns. See [Multi-level Header](#multi-level-header) for creating additional groups by hand.
#### Show Summary
The **Show summary** checkbox lives at the top of the **Visual formatting** section in the **Customize** tab, for both **Aggregate** and **Raw Records** query modes. Enabling it pins a summary row to the bottom of the grid whenever there is something to summarize: at least one metric in **Aggregate** mode, or at least one eligible numeric column in **Raw Records** mode. Otherwise no summary row is added.
@@ -386,8 +406,16 @@ The **Show summary** checkbox lives at the top of the **Visual formatting** sect
- In **Aggregate** mode, the summary row applies each metric's own aggregation (or the **Summary aggregation** override, where available) across the full filtered dataset.
- In **Raw Records** mode, the summary row defaults to a server-side `SUM` for each numeric column that's backed by a physical or calculated dataset column; the **Summary aggregation** control can override this to `AVG` as well. Non-numeric cells and columns built from free-form SQL expressions stay blank.
Changing **Summary aggregation** refreshes the summary row automatically; you do not need to click **Update chart**.
In both modes, the summary is computed across the full result set, independent of the chart's row limit and pagination, and it reflects dashboard and chart-level filters. It does not reflect AG Grid's own server-side column filters (the per-column filter UI in the grid header), which are excluded from the summary query.
#### JSON values
A text cell that holds a JSON object or array keeps an icon on the right. A column format that changes the displayed text keeps that text. In a column that wraps text, the JSON wraps onto extra lines. In a column that truncates long cells, it is shown as one line and expands while the pointer is over the cell. A click on the text selects the row and can apply a cross-filter. A click on the icon, or Enter while the cell is focused, opens the value in a dialog, with a control to copy the original text.
**Customize** → **Options** → **JSON in cell** adds an arrow on the left and keeps the value on one line. A click on the arrow expands and collapses the JSON in the row. The icon on the right still opens the dialog.
### Dynamic Currency Formatting
Chart metric values can display currencies dynamically rather than using a fixed currency code. To enable:

View File

@@ -163,7 +163,7 @@ One caveat when the host page lives on a different site than Superset: the defau
## Security Notes
- **Guest tokens expire** — their lifetime is controlled by the `GUEST_TOKEN_JWT_EXP_SECONDS` config (default: 5 minutes). Refresh tokens before they expire using a token refresh mechanism in your host app.
- **Row-level security** — pass `rls` rules in the guest token request to restrict which rows are visible to the embedded user.
- **Row-level security** — pass `rls` rules in the guest token request to restrict which rows are visible to the embedded user. Semantic views cannot enforce guest-token SQL clauses, so queries and cached-result reads are rejected when a global rule or a rule scoped to the semantic view applies. For hosts, a global guest rule deliberately refuses every semantic view, and a dataset-scoped rule can also refuse a semantic view even when the rule was intended for a SQL dataset. Read permissions do not override these restrictions. Semantic views remain accessible when no guest rule applies; SQL datasets continue to enforce their applicable rules.
- **Allowed domains** — restrict which host origins can embed a dashboard by setting **Allowed Domains** per-dashboard in the _Embed_ settings modal. Superset checks the request's `Referer` header against this list before serving the embedded view; an empty list allows any origin, so configure this explicitly for production.
- **Redacted errors** — API responses to a guest token report a generic `An error occurred while fetching the data.` instead of the underlying error, since engine errors quote catalog, schema, table and column names. Errors Superset raises itself — access denials, timeouts, payload validation — keep their message, and the full error is always available in the server logs.

View File

@@ -24,6 +24,25 @@ The following information about each flight is given:
- Information about the origin and destination.
- The distance between the origin and destination, in kilometers (km).
### Semantic Table time grains
For a Table chart backed by a semantic view, Time Grain applies when grouping by
a temporal dimension. Removing the last temporal dimension leaves the saved
choice intact, but the frontend omits a recognized dormant grain from aggregate
requests whose selected columns are known to be non-temporal (including no
grouping columns). Adding the temporal dimension again reuses the saved choice.
Unknown columns or grain values are not silently normalized. Ordinary dataset
charts and raw-record queries are unchanged.
This applies to requests rebuilt by the updated frontend in Explore and
dashboards; it does not rewrite saved charts. Direct API requests and saved
chart-data GET paths that bypass rebuilding remain subject to strict validation,
so old stored query contexts can still fail. Updated frontend assets are required.
Dashboards reuse each chart's saved `temporal_columns_lookup` rather than
recomputing it. A grouped chart saved before this change may lack the
non-temporal column metadata needed to omit a dormant grain, so its dashboard
requests can keep sending that grain until the chart is re-saved in Explore.
### Enabling Data Upload Functionality
You may need to enable the functionality to upload a CSV or Excel file to your database. The following section

View File

@@ -90,3 +90,19 @@ Use these when a metric's underlying values are stored in meters or centimeters
## Currency
Some chart types also expose currency-specific formatting, including a dynamic mode that reads the currency from a column value. See [Dynamic Currency Formatting](./creating-your-first-dashboard#dynamic-currency-formatting) for details.
## Exact decimals in SQL Lab
SQL Lab displays database `DECIMAL` and `NUMERIC` results as exact fixed-point
text, including trailing zeros and never in scientific notation. Its JSON
responses use strings so a browser does not round values such as
`12345678901234567890.123456789012345678`. Sorting remains numeric and
compares all digits; CSV exports, including streaming exports, retain those
digits and use fixed-point notation for finite decimals too. Extensions overriding
`sqleditor.extension.resultTable` replace the built-in table in SQL Lab results
and table previews. They receive decimal strings but not the built-in comparator,
so they must implement their own exact-decimal sorting. Chart and dataset
queries share the result-set loader: Decimal columns that fall back to strings
(for example, Decimals mixed with floats) also use fixed-point notation, with
non-finite Decimals converted to null. Other chart result serialization and
chart number formatting are unchanged.

View File

@@ -115,6 +115,16 @@ The `deletion_retention.purge_soft_deleted` background task purges eligible
archived objects older than the retention window. The default Celery beat
configuration schedules it daily at 00:00 in the configured Celery timezone.
The task skips purging when `SOFT_DELETE` is off or the window is zero.
Before scanning a model's rows, it checks for a purge policy. Unsupported
soft-deletable models are retained without a scan or a per-row purge attempt.
Both live and dry runs report `unsupported_models`, a map from table name to
`1` for each skipped model (not a row count), and log one warning per model.
Each skip increments `deletion_retention.unsupported_models.<table>` once per
run. Unsupported models are excluded from `would_purge` and do not count as
`cascade_failures`; supported models continue to use their existing policies.
An empty `unsupported_models` map means no registered models were skipped for
this reason. This reporting does not add purge support for host-defined models.
The window resolves in this order:
1. A host policy, when one is installed as `SOFT_DELETE_RETENTION_DAYS_FUNC`.

View File

@@ -0,0 +1,62 @@
---
title: Semantic member selections
---
<!--
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.
-->
# Semantic member selection versions
Semantic providers can declare `selection_identity_version` when changing their
member identity encoding. Cube's stable-member-ID adapter requires
`cube-member-id-v1`. Deploy the compatible core library, host, frontend and Cube
adapter together; do not roll back to title-keyed code after saving ID-based charts.
Saved Cube selections without this version are rejected, even when a saved title
happens to equal a member ID. No historical title mapping is recovered. Open each
chart in Explore, choose **Start field selection**, reselect its metrics,
dimensions, filters and sorting, then save. Reset also clears field-dependent
formatting; it does not overwrite the saved chart until Save is chosen. Native
filters and chart customizations have a separate reset in their configuration
form that clears fields, pre-filters, sorting, defaults and dependencies.
Dashboard filters retain their own saved generation. Old filter state and
unversioned external member overrides cannot borrow a chart's version. Recreate
old dashboard permalink/filter state after reselection. Cross-view overlays are
rejected unless their source identity matches the receiving semantic view;
matching display titles are not proof of matching members.
The marker is a compatibility contract, not an authorization credential. API
clients must rebuild their selections from current member IDs before supplying
`extras.semantic_selection_version` on each query. Providers without a required
version retain their existing behavior. No metadata-database migration or
automatic rewrite of saved charts is performed.
Programmatic clients must explicitly select current member IDs. Chart-data API
queries send `extras.semantic_selection_version`; name-based datasource queries
and MCP `get_table` requests send top-level `semantic_selection_version`.
Discover the value from datasource metadata (or each external metric returned by
MCP `list_metrics`). The server never adds a missing marker from metadata.
Explore query controls remain unavailable until explicit field initialization,
so legacy filter subjects cannot trigger value suggestions before reselection.
For versioned semantic views, Matrixify **All** mode with **A to Z** or **Z to A**
sorting cannot populate or refresh dimension values from suggestions. Existing
saved values are preserved. For a new selection, switch to **Members** and enter
values manually; the control displays this guidance when suggestions are unavailable.

View File

@@ -126,6 +126,32 @@ Build ad-hoc SQL datasets that can be used as the basis for charts:
> "Create a dataset from: SELECT region, SUM(revenue) as total_revenue FROM orders GROUP BY region"
> "Make a virtual dataset called 'monthly_signups' from the users table filtered to last 12 months"
### Query the Semantic Layer
Discover and query metrics across both built-in datasets and external semantic
views (when your deployment has semantic views registered), without writing SQL:
- **Discover metrics** -- search metrics by name or description across every dataset and semantic view you can access, optionally including each metric's compatible dimensions
- **Query by metric and dimension** -- get tabular results by naming metrics and dimensions instead of writing SQL, with optional filters and time ranges
- **Progressively refine a query** -- given metrics and/or dimensions already picked, see which dimensions and metrics are compatible with that selection
**Example prompts:**
> "What metrics are available related to revenue?"
> "Show me revenue by region for the last 30 days"
> "I've picked the revenue metric -- what dimensions can I break it down by?"
:::tip Recommended workflow
1. **`list_metrics`** -- search for a metric; note its `dataset_id` (built-in) or `view_id` (external semantic view), plus its `semantic_selection_version` when non-null
2. **`get_table`** -- pass that `dataset_id`/`view_id` (and, for a versioned external view, its `semantic_selection_version`) along with metric and dimension names to get results
3. **`get_compatible_dimensions`** / **`get_compatible_metrics`** -- as the user adds selections, check what can validly be combined. `get_compatible_dimensions` returns the full compatible set (including dimensions already selected), so filter out your current picks client-side; `get_compatible_metrics` already excludes selected metrics for built-in datasets but not for external semantic views
Built-in datasets accept any combination of saved metrics and groupby-enabled
columns since a SQL `GROUP BY` has no metric-level constraints. External
semantic views can impose additional compatibility rules, which these tools
surface instead of failing the query outright.
:::
### Run SQL Queries
Execute SQL directly through your AI assistant:
@@ -292,6 +318,15 @@ that those charts may need repair; deletion does not rewrite charts. References
embedded in SQL expressions are not inferred. If a chart has malformed query-context
JSON, impact reporting still checks its saved form data and other accessible charts.
### Semantic Layer
| Tool | Description |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `list_metrics` | Discover metrics by name/description across built-in datasets and external semantic views; pass `include_compatible_dimensions` to embed each metric's compatible dimensions (off by default) |
| `get_table` | Query a dataset or semantic view by metric and dimension names, with optional filters, time range, and sorting |
| `get_compatible_dimensions` | Given metrics/dimensions already selected, return the full set of dimensions compatible with that selection (may include ones already selected) |
| `get_compatible_metrics` | Given metrics/dimensions already selected, return the compatible metrics; already-selected metrics are excluded for built-in datasets, while external semantic views return whatever their connector reports and may include ones already selected |
### Charts
| Tool | Description |
@@ -320,6 +355,53 @@ JSON, impact reporting still checks its saved form data and other accessible cha
| `delete_dashboard` | Delete a dashboard by ID, UUID, or slug; leaves its charts intact (soft-deletes to trash when `SOFT_DELETE` is enabled; fails if alerts/reports are still attached, checked before editorship; otherwise returns `permission_denied` if the caller isn't an editor of the dashboard — owners, Admins, and explicitly granted editors qualify) |
| `restore_dashboard` | Restore a soft-deleted dashboard from trash by ID or UUID (slug lookup does not cover trashed dashboards); not gated on `SOFT_DELETE`, so rows trashed while it was enabled stay recoverable, but only soft-deleted dashboards can be restored — permanently deleted dashboards are unrecoverable; returns `permission_denied` if the caller can see the dashboard but isn't an editor of it, or `NotFound` if the dashboard is outside the caller's RBAC visibility entirely |
### Dashboard text components
Use `manage_dashboard_markdown` to add, update, or remove markdown tiles,
section headers, and dividers without replacing the dashboard's raw layout.
The caller needs Dashboard write permission and editorship of the dashboard.
```json
{
"request": {
"dashboard_id": 42,
"add": [
{"component_type": "header", "text": "Sales overview"},
{"component_type": "markdown", "code": "**Updated daily**", "width": 12},
{"component_type": "divider"}
]
}
}
```
Markdown tiles are appended in their own rows; headers and dividers are appended
directly to the grid or tab. Each addition can specify `target_tab` by name or
component ID (available from `get_dashboard_layout`), including nested tabs.
Without it, additions use the first top-level tab, or the grid on dashboards
without top-level tabs. Markdown width is 1–12 columns (default 4); height is in
8-pixel grid units (default 50). Width updates
must fit the space remaining beside other components in the same 12-column row;
otherwise, the error reports the available width. Components removed in the same
request do not count toward that row width. If any operation is rejected, the
layout remains unchanged.
The response includes generated `added_component_ids` and a `components` summary.
Reuse those IDs in `update`, for example
`{"id": "MARKDOWN-…", "code": "Revised notes"}`, or in `remove`, for example
`["MARKDOWN-…"]`. Updates merge only supplied non-null fields and cannot change a
component's type or placement. Each update requires at least one non-null field
besides `id`; ID-only or all-null updates are rejected. The reserved dashboard-title `HEADER_ID` is not a
section header and cannot be changed through this tool.
Operations in one request are validated before saving. Existing charts, native
filters, and unrelated dashboard properties are preserved. Removing a tile prunes
empty row/column wrappers but retains tabs. Invalid component IDs, incompatible
fields, invalid layouts, and conflicting update/removal operations return errors
without saving partial changes. Markdown uses the dashboard's existing renderer
and HTML sanitization configuration. Concurrent layout edits are last-write-wins;
avoid simultaneous edits from the UI and MCP clients. This tool does not reorder
or move existing components; use the dashboard editor for those operations.
### SQL
| Tool | Description |
@@ -328,6 +410,14 @@ JSON, impact reporting still checks its saved form data and other accessible cha
| `save_sql_query` | Persist a SQL query to SQL Lab's saved queries |
| `open_sql_lab_with_context` | Open SQL Lab with a pre-populated query |
`execute_sql` returns the last data-bearing statement's `rows`, `columns`, and
`row_count` at the top level. Each entry in `statements` retains execution
metadata, but only earlier data-bearing statements include `data.rows` and
`data.columns`. The last data-bearing statement's `data` is `null`, so its result
is not repeated, even if DML statements follow it. Statements without result data
also have `data: null`. For each statement, `executed_sql` is `null` when it equals
`original_sql`; use `original_sql` in that case.
### Databases
| Tool | Description |

View File

@@ -58,7 +58,7 @@
"@fontsource/inter": "^5.3.0",
"@storybook/addon-docs": "^10.6.0",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.16.2",
"@swc/core": "^1.16.12",
"antd": "^6.6.5",
"docusaurus-plugin-openapi-docs": "^5.2.0",
"docusaurus-theme-openapi-docs": "^5.2.0",
@@ -83,8 +83,8 @@
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"oxfmt": "^0.70.0",
"oxlint": "^1.85.0",
"oxlint-tsgolint": "^7.0.2002",
"oxlint": "^1.86.0",
"oxlint-tsgolint": "^7.0.2003",
"typescript": "7.0.2",
"webpack": "^5.111.1"
},

View File

@@ -686,6 +686,7 @@
"pypi_packages": ["pydoris"],
"connection_string": "doris://{username}:{password}@{host}:{port}/{catalog}.{database}",
"default_port": 9030,
"notes": "The SSL switch sets ssl_mode=VERIFY_CA. With MariaDB Connector/C, which mysqlclient uses in the Superset image, this also verifies the server hostname. Add ssl_ca=<path to the CA certificate> in Additional parameters and use a Doris FE certificate that matches the host. A Doris FE on its default self-signed certificate fails to connect once the switch is on.",
"parameters": {
"username": "User name",
"password": "Password",
@@ -6964,6 +6965,25 @@
},
"notes": "Uses the Data API for serverless access. Standard PostgreSQL connections also work with psycopg2.",
"categories": ["Cloud - AWS", "Hosted Open Source"]
},
{
"name": "ClickHouse Managed Postgres",
"description": "Managed PostgreSQL from ClickHouse.",
"logo": "clickhouse.png",
"homepage_url": "https://clickhouse.com/cloud/postgres",
"pypi_packages": ["psycopg2"],
"connection_string": "postgresql+psycopg2://{username}:{password}@{host}:{port}/{database}?sslmode=verify-full&sslrootcert={ca_certificate_path}",
"parameters": {
"username": "Database user",
"password": "URL-encoded database password",
"host": "Direct PostgreSQL hostname from the Connect dialog",
"port": "Port from the Connect dialog (default: 5432)",
"database": "Database name",
"ca_certificate_path": "URL-encoded absolute path to the instance CA certificate on the Superset server"
},
"notes": "In ClickHouse Cloud, open the Connect menu and use the Directly option. Download the instance-specific CA certificate from Settings and make it available to each Superset process that connects to the database. Use verify-full to verify the certificate and hostname.",
"docs_url": "https://clickhouse.com/docs/products/managed-postgres/connection",
"categories": ["HOSTED_OPEN_SOURCE"]
}
],
"category": "Traditional RDBMS",

View File

@@ -1245,6 +1245,11 @@
],
"type": "string"
},
"semantic_selection_version": {
"description": "Identity encoding used for semantic member selections.",
"nullable": true,
"type": "string"
},
"time_grain_sqla": {
"description": "To what level of granularity should the temporal column be aggregated. Supports [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) durations.",
"example": "P1D",
@@ -3686,7 +3691,7 @@
"CurrentUserPutSchema": {
"properties": {
"current_password": {
"description": "The current user's existing password",
"description": "The current user's existing password; required when the account has one",
"type": "string",
"writeOnly": true
},
@@ -3703,7 +3708,7 @@
"type": "string"
},
"password": {
"description": "The current user's password for authentication",
"description": "The current user's new password; requires current_password when the account already has one",
"type": "string"
}
},
@@ -7087,6 +7092,12 @@
"arrow"
]
},
"semantic_selection_version": {
"default": null,
"description": "Identity version from datasource metadata, after selecting its current metric and dimension IDs. Never infer legacy IDs.",
"nullable": true,
"type": "string"
},
"time_column": {
"default": null,
"description": "Temporal column the time range applies to. Inferred from the datasource when omitted.",
@@ -19732,7 +19743,7 @@
},
"/api/v1/dashboard/{pk}/export_as_example/": {
"get": {
"description": "Exports a dashboard with its charts and datasets in the example format used by the Superset example loading system. The export includes Parquet data files and YAML configuration files.",
"description": "Exports a dashboard with its charts and datasets in the example format used by the Superset example loading system. The export includes Parquet data files and YAML configuration files. Charts and native-filter targets that use semantic views are not supported.",
"parameters": [
{
"description": "The dashboard id",
@@ -19782,6 +19793,9 @@
"404": {
"$ref": "#/components/responses/404"
},
"422": {
"description": "The dashboard contains unsupported semantic-view assets"
},
"500": {
"$ref": "#/components/responses/500"
}
@@ -25326,6 +25340,13 @@
]
},
"type": "array"
},
"suggestions_status": {
"description": "Suggestions are disabled for versioned semantic views; enter values manually.",
"enum": [
"unavailable_versioned_view"
],
"type": "string"
}
},
"type": "object"

View File

@@ -3173,130 +3173,130 @@
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.70.0.tgz#6d8d33d38063498ad9ead3b3f9f1dd2aa6ce03be"
integrity sha512-HlIZEn+WzLQL0DszNzldiRl/DPRCX5R0Vkt6qeUPR1YHwy52hZZo4x6HoTOVmKRP2wUiwPGtKsihNY/f8KRaBg==
"@oxlint-tsgolint/darwin-arm64@7.0.2002":
version "7.0.2002"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2002.tgz#75cc95455005802d26065af49fd47dfb32830a36"
integrity sha512-E3yYb/tI+5M3SqGgKwrwUD0nmjA2FMGD1xFbma46qi8Y7+nPTlGY6bZk/hqxijc9Vda/tdtUxQ2d5zGIEX9qlg==
"@oxlint-tsgolint/darwin-arm64@7.0.2003":
version "7.0.2003"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2003.tgz#a9048bb88e36fb7d3523d4c444509d96e06d5d46"
integrity sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w==
"@oxlint-tsgolint/darwin-x64@7.0.2002":
version "7.0.2002"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2002.tgz#fea67ca1fe83d25d22d2378b347a9a751d7ca4a0"
integrity sha512-qEaMySdvzs8faaJwMO4dFtPFCa+degFCz+HbkFQ0REcIJqJ16KmC0smt30FAI9TeEHCoAtz6sU/6acJvi4uaxg==
"@oxlint-tsgolint/darwin-x64@7.0.2003":
version "7.0.2003"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2003.tgz#34933fbc2f36c418591808a75a4a164eb83afdf0"
integrity sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg==
"@oxlint-tsgolint/linux-arm64@7.0.2002":
version "7.0.2002"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2002.tgz#f2d49e0c76b7f257ebb10f902201c3dde7ff0e46"
integrity sha512-bqPObCNIR6tDQSc0Xn9Nu7EfWRXFB+qFzADRl/q6YA8zT9YUg3pGUbmicV2hV91uLpiot74so9ocGjx35SaX6w==
"@oxlint-tsgolint/linux-arm64@7.0.2003":
version "7.0.2003"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2003.tgz#b222cb1f8366ea7de441551268301efa82efc379"
integrity sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw==
"@oxlint-tsgolint/linux-x64@7.0.2002":
version "7.0.2002"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2002.tgz#397d85a072b9d4a38a4d61c96a85f45127024d89"
integrity sha512-E5CcRALAYdiQMbdYjEqlLOiYuEdt21RtXiyxIoD8bfypdfkpWppacijKOR4i0DtLXFRVwCGQggZBWk5bOIpU1A==
"@oxlint-tsgolint/linux-x64@7.0.2003":
version "7.0.2003"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2003.tgz#1e1339078dd464757f543eca9a60dc9d86ad6e75"
integrity sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw==
"@oxlint-tsgolint/win32-arm64@7.0.2002":
version "7.0.2002"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2002.tgz#2701a93c74484c3b560090ca1ce78b4c7d8ffaab"
integrity sha512-L+ul0ZFDiz/ZmTwCQ1J/qS7S6s45ktiaFqb+ZIzx+1sSdtdRxm8/H0+p4VDQYrCKBFzKNb6VjUlm48bNEe3BUg==
"@oxlint-tsgolint/win32-arm64@7.0.2003":
version "7.0.2003"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2003.tgz#b4d0e6c6e7c0212be92c447e64b49f0d8d8173db"
integrity sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q==
"@oxlint-tsgolint/win32-x64@7.0.2002":
version "7.0.2002"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2002.tgz#6d3a6c4d9229765ad45eb2ecdecd8d1ceffefee4"
integrity sha512-zkZeJfo4UmYiCzj77u85eCDpdxOiQiSAsV6j9Pvj5e7Vu5+MvwLtK1xmVutE77tJmUJ3eBOSXHR6WgY4AwHHGQ==
"@oxlint-tsgolint/win32-x64@7.0.2003":
version "7.0.2003"
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2003.tgz#08d5ae59e655e32d332ec1a74190b8009754d176"
integrity sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA==
"@oxlint/binding-android-arm-eabi@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.85.0.tgz#b95305795de282c081492653abada2a46f68c714"
integrity sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg==
"@oxlint/binding-android-arm-eabi@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.86.0.tgz#53c7e6c799761229eddfd16ae0cf4df852d72afc"
integrity sha512-63Ozq6yQn79B2UxjSZ9huJmI+3/ZPI7hxAe6TyFh9oQL447KnIMoffOdtog+h4KILbQr61ZRvFPW5gYbAUgpgQ==
"@oxlint/binding-android-arm64@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.85.0.tgz#afe58d09f29ba7a56415fefcf5a55d6baa758d50"
integrity sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA==
"@oxlint/binding-android-arm64@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.86.0.tgz#7e4b00a16f9849a76757d4aee825a4961e4cb308"
integrity sha512-JdLGp44phbf/0Du236Dt06yfK6Qw8xhTFeDvHJdjk167r59OfP5+BL31mgMYVnswS8e+j6tFuccSY0QDC/lgCw==
"@oxlint/binding-darwin-arm64@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.85.0.tgz#d63f7e3fefa7073edd5a42b0a6ad07a458fbffef"
integrity sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA==
"@oxlint/binding-darwin-arm64@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.86.0.tgz#12b0d390e913da1ebb14190f9cbcb6e366c7525d"
integrity sha512-h+vkOr4ik6KLFCXdtNVEj9xfnXfNUpPoF95QjORdhiSpLqQePzcN81kGHjAgsw1/G7WBWK5KsFKbI2UK8EyzMA==
"@oxlint/binding-darwin-x64@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.85.0.tgz#41c75b0181f360b7cc2654e478c95037be780708"
integrity sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw==
"@oxlint/binding-darwin-x64@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.86.0.tgz#864c33d7aa3c0e4547604054fe4101e00f61685b"
integrity sha512-wbglRuH5eKsp68keTOOeKDHU1astWSRYFsh2bwKQTAPb44rD6kVgzYI1ZrAJRTc6/7PH55ZXFCQx0rQXrj0UdA==
"@oxlint/binding-freebsd-x64@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.85.0.tgz#252b099dd4c4c3d5ee7fc53a774d58a37ac6a948"
integrity sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg==
"@oxlint/binding-freebsd-x64@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.86.0.tgz#62b09121f28b3ed7664dc718ad3cd15acf6153bb"
integrity sha512-yTQ7QSOlNfQsrfbeVweTiegQJLKPjUKgXp/JpGiFzza6Hp29S9LAoH77TM3L0RjK39iQRZB5lk5iq2t241Mfag==
"@oxlint/binding-linux-arm-gnueabihf@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.85.0.tgz#ae16c5b5f55480dba86ce7de377ede00964ea756"
integrity sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw==
"@oxlint/binding-linux-arm-gnueabihf@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.86.0.tgz#81f07f56e0e69d26387be6a1a185512873642737"
integrity sha512-QYJGy0E2Tv8GAayyBIeHFTwQb8u+zrvrOYrifuDGZDr9ad5XsDhiMScpZymhS9fwWjqrgG2ZUwT4vpY0ISqp4Q==
"@oxlint/binding-linux-arm-musleabihf@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.85.0.tgz#a903f6043725cc311c81e2c5a1e3383621b0d2d9"
integrity sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g==
"@oxlint/binding-linux-arm-musleabihf@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.86.0.tgz#7a3a20e9f5bb04a4d0d4111bd300b7cf8dcb85a4"
integrity sha512-oQo83CafLT8p3qh0AGkN0tSUIh5Hm63mfDsoqUmBQUBnSZLNBAbyRaAftfGY5a0cKZ34dG6jAtMYgcY5kZqt5g==
"@oxlint/binding-linux-arm64-gnu@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.85.0.tgz#8afee8ea5be0c4edb74d2a25eee1e510c77dbf8e"
integrity sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg==
"@oxlint/binding-linux-arm64-gnu@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.86.0.tgz#b876d20c845fb14f89a3e2bcdff4be8d127c9169"
integrity sha512-EM6wy5c2UM12qPb7iDJDBAZvHyfEJGH6iosFgaFiaRZpirz99//EHcUyD2KAskz07x1TULuiAtsojf4nvWWksw==
"@oxlint/binding-linux-arm64-musl@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.85.0.tgz#62cc783fb610f1b0be9328364eec8a07add3e3e7"
integrity sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw==
"@oxlint/binding-linux-arm64-musl@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.86.0.tgz#c9ee28093e7524364b918e1cd025286ff4c2fa82"
integrity sha512-vCiUQb9ZNzZalxYRU4mlZwUqT5f+c7Sk9DwUOplWHtik3Dl4r3DVnUB5tBeRuTAUe6MUVCTylNW7TkkN8J1oTw==
"@oxlint/binding-linux-ppc64-gnu@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.85.0.tgz#530ec6f185f8bc7f86819b44483e987599ccbcf1"
integrity sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw==
"@oxlint/binding-linux-ppc64-gnu@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.86.0.tgz#7964bf1e7593403e6b5013eece02cd3018a7ed1b"
integrity sha512-T0AzE89yjTYmOhB/hb6i+rWTpe2mYt+xiguBhs9DEPIJTZ1H8OYqsE4VxJHHw2Wlq8Mztwvp2P8NV87ZOuhPRw==
"@oxlint/binding-linux-riscv64-gnu@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.85.0.tgz#35c430e60ef8d8dddafd16f79adc1bf936ddeddb"
integrity sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg==
"@oxlint/binding-linux-riscv64-gnu@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.86.0.tgz#dd843ff3e41715311a30ce693ab4df91150dcb38"
integrity sha512-s49r0K2f5Oc7fJoMUO1WkdhYK5Ob79uYRwWQ/XNoGysaOq3RUGmGUbHSuXiyydTTA+P9xEdDEnnlZVXiaSJhlw==
"@oxlint/binding-linux-riscv64-musl@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.85.0.tgz#426eb8fb43852707dd872bbc0e8415832f9307e8"
integrity sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA==
"@oxlint/binding-linux-riscv64-musl@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.86.0.tgz#7bb8bdb9c03b651f57f868ed2279f940ef19b8fc"
integrity sha512-JIa2tYR8ayb3nvcMSZzn4O/xTy13S7MHllu7cBD+B5qDQXXSRy/Vwk1aNpiTjZmBJGwKCHOy/NvnY5Xcwg27uQ==
"@oxlint/binding-linux-s390x-gnu@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.85.0.tgz#f53c7132389e71dc68cad7a319fc0a92abbb1fcf"
integrity sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ==
"@oxlint/binding-linux-s390x-gnu@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.86.0.tgz#14aba52915f8df19b2802a5ec5d2308ff8f2ddb2"
integrity sha512-XwXMyBsAuWZkhZU74UNcfQKr1CJJAKnS80dZzOnjl6A4fKcf91IjFgidegQugMHA5SN8ulwDXEDV1yPntjXgQQ==
"@oxlint/binding-linux-x64-gnu@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.85.0.tgz#74769aa504ce7c2571574684ed36387b40c9bfb7"
integrity sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg==
"@oxlint/binding-linux-x64-gnu@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.86.0.tgz#6bb162b2359860ec6c0cb1d0ad7001045ac21d7e"
integrity sha512-C1WjukSyMnr66b+w1/tV8RFVv6d9v0MzDf4p9IxVXknqgmTHBgZh1pccN1eHzFr0b9Tbb3OXoPsAAdAuHAQfeA==
"@oxlint/binding-linux-x64-musl@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.85.0.tgz#0d99815e87e85c51c4ab0e351ddf9b643578e3b6"
integrity sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA==
"@oxlint/binding-linux-x64-musl@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.86.0.tgz#9235a290fc704fb75ec62302c4f935f5ee966d33"
integrity sha512-ap6KLmvC38c6MdYzsIh25cXQupqYvjd37tMNftzrX1DCtkX1Gcf2B+S2B17dD4lKa5c5gJog7DQJyDo82PBKyw==
"@oxlint/binding-openharmony-arm64@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.85.0.tgz#051fc72cd1a103b73d758f3f2eb9d3748ba4aafc"
integrity sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA==
"@oxlint/binding-openharmony-arm64@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.86.0.tgz#d7ebd7d549912d1dd567a9f0778e9a270021244e"
integrity sha512-996Q9w/2GLwNyy5Djs39Jt09aFlr4b7TPhoj7dvo5FHmcyiF68nPz73HNc5UAYKIYeK+M9JzPQ4Wg1r1dXvP6A==
"@oxlint/binding-win32-arm64-msvc@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.85.0.tgz#6bd2fd33ae029685f0f162c8ba5976c3e2059a6b"
integrity sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA==
"@oxlint/binding-win32-arm64-msvc@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.86.0.tgz#902c2cd7292dfaf0d889a094806d337848ba7486"
integrity sha512-9uTCHYTknNkug5+46ViSoUMuOuGWK61D87QnblU6CXaWKJC630Z7dbls61vdHxya/cR2ycQZ6Mm6ckgF6Cag5g==
"@oxlint/binding-win32-ia32-msvc@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.85.0.tgz#d8ab96fd47c575baf78713425680e0533aa2436f"
integrity sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A==
"@oxlint/binding-win32-ia32-msvc@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.86.0.tgz#f06500f4b1c3abac324710eded785537aa6799e3"
integrity sha512-a47ga7EKVfMU8rO0i1m3mGwfNRj3FhQyt1d/ukO/E9u1HHF3KpR3tyb3VhuJBGEVPF8uR4exSyHUyx7xbE+uxw==
"@oxlint/binding-win32-x64-msvc@1.85.0":
version "1.85.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.85.0.tgz#d8ab9af17878dfee4f6b57581eb116cb10a9abd6"
integrity sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA==
"@oxlint/binding-win32-x64-msvc@1.86.0":
version "1.86.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.86.0.tgz#bef9b82d6408a5a228b0e31902c8bdd482ad169c"
integrity sha512-az4dzifCRzES1crwu6pDFXzJTIanrdxXFL9TbiJWrx9LKcuacbR83aPvxDA1LKLNswi/RoqitC7Afcflhu3zBQ==
"@parcel/watcher-android-arm64@2.5.6":
version "2.5.6"
@@ -4830,86 +4830,86 @@
dependencies:
apg-lite "^1.0.4"
"@swc/core-darwin-arm64@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.2.tgz#b180d8647923c75e5aabbf4ea6e543d113aa3c79"
integrity sha512-i/j0HNbnn79qnTVPicvay92Nark8fW8NQqn1e2mGERjUXNpBV0+SwQxlRpk2zBhn6laJ8PDI6Kn1nHZhnz3LCA==
"@swc/core-darwin-arm64@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.12.tgz#925a647db6fb235f6d7dff0e3c886ede8ad7fa55"
integrity sha512-eu616yP7s+M+crBEEO8Pg8DooMNliTQrXD4NyiBCXIFGSNSEbn4kQRACWuR2O+OyoMBwk6FXL3zFVvR+iJs/XA==
"@swc/core-darwin-x64@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.16.2.tgz#ae8286ddd5a6f1b0f1c540e3338682fed7ceb2ab"
integrity sha512-HrwqHyEyHVXO3qTk8EkNK7/b6sOZSEoNh+pot6RdE5x0LbNqfo8LtJUvi3UTXr+5ja/o5HbJdW80eCXo+NjbiA==
"@swc/core-darwin-x64@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.16.12.tgz#642d80a8f99f4e296b0e23918169ea6c06ab4fed"
integrity sha512-vOT0kyUHp0lLBrcN0/ycxtOaVEttt7TLO7ztPrDhAPn26qq1g7vp+1xA5Dp5v9Woj+6+bS3YPCIdhhkgqXcMcg==
"@swc/core-linux-arm-gnueabihf@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.2.tgz#9879d6eb91d3663dfcae557cde866642b21b7590"
integrity sha512-MdXi83Z/gGp1LIrg+h7HKxiul/z/Bty/ZJSvYAFqDl9zteC1XLSAZdScquKtXPp50rdyXqritTDCqQBhwVfZKA==
"@swc/core-linux-arm-gnueabihf@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.12.tgz#9897f5c66151183ec0c664bc237294d0a1a6822c"
integrity sha512-FVA9a7sxeisBzSh+z+4em/3GUhzzU7E1wjGl34RLklzeznoKZxjRCgnJGr4VjeCLeL+hh46qQtbj5LcMIzPi0w==
"@swc/core-linux-arm64-gnu@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.2.tgz#9684df59917e4b0de8f528f04cf7f6e7526ca7b1"
integrity sha512-/jcTmK6Ktz3owM3YtiKvjofV6p3VpHnYzTIrOGwDIOsDigRAAVuZ8east33wYO/7UTdKYFlyHNnJNT0WJqOA3Q==
"@swc/core-linux-arm64-gnu@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.12.tgz#d610b9168fcc568dec24eab77d41873edd09437a"
integrity sha512-K0RcZG1Xy0A+DprzW0A2z7vM/p5cPV7qYvv/LjnwRfS6Fph9HrI8PnplVycKrwLWfNyAcMrurFsYDWVPALJnwQ==
"@swc/core-linux-arm64-musl@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.2.tgz#36b59ed1a46795755e6d083e6693753c5ea1e7d0"
integrity sha512-4gFarKaFnlJTSlJYKmMhV4u+3YE4uYfiydpBoYjmgQhCf9lAieOq+WilZaK9vVSHeqLuQpTEiGULZqAdsRX5Dw==
"@swc/core-linux-arm64-musl@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.12.tgz#77b57cb2fb87891a9c1b72349480f95545a51a50"
integrity sha512-nqXzwLk9aLb3ZH4r5WVtn8B3RKLFqYjy/ApK753QYYoQvnYJipKnF09vUI9m65roOPdf/wt+LKvZ72EgUvPBiw==
"@swc/core-linux-ppc64-gnu@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.2.tgz#b0f648e760031e4674e0d07eb4991dfb468d21ee"
integrity sha512-syqSLGd6KlZ1PciNzs6bIUlhOuFztZufebOHaERjc4N4SqNZxyqYd4I+jj/EfOYnpe0kNjccn9HJLN1p5dz3+w==
"@swc/core-linux-ppc64-gnu@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.12.tgz#2d51317abae9c297a8dace3f7cbab8b2d444580a"
integrity sha512-kJab4XOOe590gTj2HOEqIrKJjwNaJBZQsd1fn1yYYg3C0YMG8kgXsEsIpZNgdxw8LsUeeS7iwUSSwEHY3PVSEQ==
"@swc/core-linux-s390x-gnu@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.2.tgz#7757e4999f4e236a8bb05bd04367574ce926d4cc"
integrity sha512-ZBBLK+ewGyXLzWeMS7wbKtWBdnif6etn7xvPY/iOfbdsjX/+bgkp1pQt2lWF2wlu2hXYZuhJ/tHZE/QR8/apzg==
"@swc/core-linux-s390x-gnu@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.12.tgz#2659cee9526ad644945874a8ffdcc30335462663"
integrity sha512-FWttNqogiRwZwmqRdPC7vEBa9HV9RbZBoUqLP0CbWhUPmf6TxYTRTjh9JA5EAsAlRJ7YiwXSoCT0ersJfTUiXw==
"@swc/core-linux-x64-gnu@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.2.tgz#a25271e8199c46e54f1042c9d98fbf38fdb4736c"
integrity sha512-LyHJgxCA4Tje0ysBMbEb0tt/ie8kgUKoFE3JAKFhpevmTmhYEoC0H9s47WuDsqiFckF1ITUguZIXJG6K5e0dvg==
"@swc/core-linux-x64-gnu@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.12.tgz#f127eeaa6602a61fa13a2f072b79b9987186b4ad"
integrity sha512-8JJ0qouw0h8IXNXj+GLL8kPwcjhPI11/mW4ocmeweMJdC/WjuDQGaDbYErKRqwbkTUjArLx/938CQa2ulUtCnw==
"@swc/core-linux-x64-musl@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.2.tgz#afc4e5f5e519e4e2ca75a560496fbc6b5164718d"
integrity sha512-PghXJlVM1cgtLfNUR1vxFo1z+PDRAe8cWAJlZZ7spmeiN7BospGXg/MHUg7oNSgwSX7Zo//YKv9P5yD9apsFJQ==
"@swc/core-linux-x64-musl@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.12.tgz#978eed0a392b3341cad7152cb5d88b00e4fec2da"
integrity sha512-0ev8zRnVSaqJEi63reVHyv81Xce1x6BhoASBPX4nd3recoExu+2Hlp70kQThe1MDohfyYzksErwjPb1Nj0rCFQ==
"@swc/core-win32-arm64-msvc@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.2.tgz#98b936a9cf2faad87c92c979161f17f22c55b742"
integrity sha512-StTOSefYBxemvNYYUI3UmO1a8y+hSPjjfHogC2TEHL+Z1PlEBim/XtLas5rS04jAzT9RrNmbtX911SZ42H9jSQ==
"@swc/core-win32-arm64-msvc@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.12.tgz#24c6bab47eda721780db54843929b89d0db2e9d5"
integrity sha512-0Cje91Dhwv18EwK4EVJ6UVFsaz19ndeHvmR+55UkENe3CLxq5Qk6+KkFSbxFgfHwYwXmiMpVVJt5MjfEsXy8yA==
"@swc/core-win32-ia32-msvc@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.2.tgz#2e30f6a8f78b0c79268e6d39f0ae3b378919449c"
integrity sha512-fycER209DYIzsibpTMC+chND05OfOjgztWL9U8OE6/uUlsOUZH3eh98isBLEnOymYUhlJLEt5++W1+KL/FOh5Q==
"@swc/core-win32-ia32-msvc@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.12.tgz#94400b876320acd0827bedaf31f5726a0f82622a"
integrity sha512-1kVJta17y80uk0eBOQ5UYpr3JvYgg4TxPO4nAErz34f7ATE7ZSnVnMRHC4+QqFNZLdbtww/QqwhQW1El7pcUkQ==
"@swc/core-win32-x64-msvc@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.2.tgz#a25e080ed09770e68c1dc5a2a7e973a01dc40b2f"
integrity sha512-cSd1z6ivSrJPVr+moVwOHWjeKy6TpO4/Shwcv5KCrKYXCccxwh4pRy1C3fDioNx2PF1jPZWHKZjtXt+Be9VbaQ==
"@swc/core-win32-x64-msvc@1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.12.tgz#e092d6fe1de40e4975693fba7c57a7cb693c896d"
integrity sha512-urCL+y3TohYUEwlXDjhfqxSUU/17N8iCZdhK6HxGXZgMQB1fK6nSu4zQ33oh2mBvFnIXbG6VM9eK3QYrepPc3A==
"@swc/core@^1.15.40", "@swc/core@^1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.16.2.tgz#6cc0c7f27938bea7208467a18ab518fa23b78367"
integrity sha512-95I4kiSMeveI/Mhi+tE4fiWcWLUMfzfKrk0jtr8LRMqHgOgq+xHS+zExkDqoO4b5OeeuXHMWVdD5MeP3X6sULw==
"@swc/core@^1.15.40", "@swc/core@^1.16.12":
version "1.16.12"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.16.12.tgz#92693396dca6e9d88a763994be447ad907d8e8f6"
integrity sha512-ksgw/DvuDrxAFi7tfdndxKSyoAls7VNkIlqlASySpe0tbPmvMH2JcAoHMbRZyE6f10EAf289blkvbe/KCOblxw==
dependencies:
"@swc/counter" "^0.1.3"
"@swc/types" "^0.1.28"
optionalDependencies:
"@swc/core-darwin-arm64" "1.16.2"
"@swc/core-darwin-x64" "1.16.2"
"@swc/core-linux-arm-gnueabihf" "1.16.2"
"@swc/core-linux-arm64-gnu" "1.16.2"
"@swc/core-linux-arm64-musl" "1.16.2"
"@swc/core-linux-ppc64-gnu" "1.16.2"
"@swc/core-linux-s390x-gnu" "1.16.2"
"@swc/core-linux-x64-gnu" "1.16.2"
"@swc/core-linux-x64-musl" "1.16.2"
"@swc/core-win32-arm64-msvc" "1.16.2"
"@swc/core-win32-ia32-msvc" "1.16.2"
"@swc/core-win32-x64-msvc" "1.16.2"
"@swc/core-darwin-arm64" "1.16.12"
"@swc/core-darwin-x64" "1.16.12"
"@swc/core-linux-arm-gnueabihf" "1.16.12"
"@swc/core-linux-arm64-gnu" "1.16.12"
"@swc/core-linux-arm64-musl" "1.16.12"
"@swc/core-linux-ppc64-gnu" "1.16.12"
"@swc/core-linux-s390x-gnu" "1.16.12"
"@swc/core-linux-x64-gnu" "1.16.12"
"@swc/core-linux-x64-musl" "1.16.12"
"@swc/core-win32-arm64-msvc" "1.16.12"
"@swc/core-win32-ia32-msvc" "1.16.12"
"@swc/core-win32-x64-msvc" "1.16.12"
"@swc/counter@^0.1.3":
version "0.1.3"
@@ -6957,13 +6957,14 @@ compressible@~2.0.18:
mime-db ">= 1.43.0 < 2"
compression@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79"
integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==
version "1.8.2"
resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.2.tgz#781397127ae2da00c0c5f7c76bda5b2a9b32a49f"
integrity sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==
dependencies:
bytes "3.1.2"
compressible "~2.0.18"
debug "2.6.9"
destroy "1.2.0"
negotiator "~0.6.4"
on-headers "~1.1.0"
safe-buffer "5.2.1"
@@ -9100,9 +9101,9 @@ htmlparser2@^8.0.1:
entities "^4.4.0"
http-cache-semantics@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5"
integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==
version "4.3.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz#09eead3b16c6d85552857cc3fbd888d8353a2aaf"
integrity sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==
http-deceiver@^1.2.7:
version "1.2.7"
@@ -9600,9 +9601,9 @@ jiti@^1.20.0:
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
joi@^17.9.2:
version "17.13.7"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.7.tgz#92e212c50dbbbcb1a1592424f84083eb265cc778"
integrity sha512-MF80Dm5Y2veNy8QWVx9Bj3ui4mo7+VPSPsR1M+oaHXV0Gx6zGX9a2F+OZG3Blby9tOlzU9Rs5FUimlEhbKtfnQ==
version "17.13.8"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.8.tgz#92069d521eea09fd709d8abbb86b0ef02862224f"
integrity sha512-iPKOGmiRw1jxf/JOPwxmCcUQAOdF359mdzYiP2DJ+TMX0YK2zjK3D+zYOaGjpumWxOFF/l2xVWjRVK5bGSLdEw==
dependencies:
"@hapi/hoek" "^9.3.0"
"@hapi/topo" "^5.1.0"
@@ -11517,42 +11518,42 @@ oxfmt@^0.70.0:
"@oxfmt/binding-win32-ia32-msvc" "0.70.0"
"@oxfmt/binding-win32-x64-msvc" "0.70.0"
oxlint-tsgolint@^7.0.2002:
version "7.0.2002"
resolved "https://registry.yarnpkg.com/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2002.tgz#ac3ba9afb73193dcaabf2d4121e83bbf87571396"
integrity sha512-rQAj2RAQM57nCwTd3PhtnAkREGW2StqI47tzudNlJF8WwGommM7rVpeP4O1O8/ZiuDZ9f2GnTESzLHGTluwkvQ==
oxlint-tsgolint@^7.0.2003:
version "7.0.2003"
resolved "https://registry.yarnpkg.com/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2003.tgz#ad35592d0fde9ffd279c9befe3275a81a787573f"
integrity sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ==
optionalDependencies:
"@oxlint-tsgolint/darwin-arm64" "7.0.2002"
"@oxlint-tsgolint/darwin-x64" "7.0.2002"
"@oxlint-tsgolint/linux-arm64" "7.0.2002"
"@oxlint-tsgolint/linux-x64" "7.0.2002"
"@oxlint-tsgolint/win32-arm64" "7.0.2002"
"@oxlint-tsgolint/win32-x64" "7.0.2002"
"@oxlint-tsgolint/darwin-arm64" "7.0.2003"
"@oxlint-tsgolint/darwin-x64" "7.0.2003"
"@oxlint-tsgolint/linux-arm64" "7.0.2003"
"@oxlint-tsgolint/linux-x64" "7.0.2003"
"@oxlint-tsgolint/win32-arm64" "7.0.2003"
"@oxlint-tsgolint/win32-x64" "7.0.2003"
oxlint@^1.85.0:
version "1.85.0"
resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.85.0.tgz#a783acb92bd70986b2586a7457984126daf4e52e"
integrity sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg==
oxlint@^1.86.0:
version "1.86.0"
resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.86.0.tgz#6b5d3f095eb32a8003839bffaf39e4203df644e3"
integrity sha512-og0lhgvZfgGF//gOOmZXvtr+GmBbAGEnbEhv/QUg7UW2Wi4wHMnJbnMD+zuHgPdJxdklfgpPupdlAaAySyxrZg==
optionalDependencies:
"@oxlint/binding-android-arm-eabi" "1.85.0"
"@oxlint/binding-android-arm64" "1.85.0"
"@oxlint/binding-darwin-arm64" "1.85.0"
"@oxlint/binding-darwin-x64" "1.85.0"
"@oxlint/binding-freebsd-x64" "1.85.0"
"@oxlint/binding-linux-arm-gnueabihf" "1.85.0"
"@oxlint/binding-linux-arm-musleabihf" "1.85.0"
"@oxlint/binding-linux-arm64-gnu" "1.85.0"
"@oxlint/binding-linux-arm64-musl" "1.85.0"
"@oxlint/binding-linux-ppc64-gnu" "1.85.0"
"@oxlint/binding-linux-riscv64-gnu" "1.85.0"
"@oxlint/binding-linux-riscv64-musl" "1.85.0"
"@oxlint/binding-linux-s390x-gnu" "1.85.0"
"@oxlint/binding-linux-x64-gnu" "1.85.0"
"@oxlint/binding-linux-x64-musl" "1.85.0"
"@oxlint/binding-openharmony-arm64" "1.85.0"
"@oxlint/binding-win32-arm64-msvc" "1.85.0"
"@oxlint/binding-win32-ia32-msvc" "1.85.0"
"@oxlint/binding-win32-x64-msvc" "1.85.0"
"@oxlint/binding-android-arm-eabi" "1.86.0"
"@oxlint/binding-android-arm64" "1.86.0"
"@oxlint/binding-darwin-arm64" "1.86.0"
"@oxlint/binding-darwin-x64" "1.86.0"
"@oxlint/binding-freebsd-x64" "1.86.0"
"@oxlint/binding-linux-arm-gnueabihf" "1.86.0"
"@oxlint/binding-linux-arm-musleabihf" "1.86.0"
"@oxlint/binding-linux-arm64-gnu" "1.86.0"
"@oxlint/binding-linux-arm64-musl" "1.86.0"
"@oxlint/binding-linux-ppc64-gnu" "1.86.0"
"@oxlint/binding-linux-riscv64-gnu" "1.86.0"
"@oxlint/binding-linux-riscv64-musl" "1.86.0"
"@oxlint/binding-linux-s390x-gnu" "1.86.0"
"@oxlint/binding-linux-x64-gnu" "1.86.0"
"@oxlint/binding-linux-x64-musl" "1.86.0"
"@oxlint/binding-openharmony-arm64" "1.86.0"
"@oxlint/binding-win32-arm64-msvc" "1.86.0"
"@oxlint/binding-win32-ia32-msvc" "1.86.0"
"@oxlint/binding-win32-x64-msvc" "1.86.0"
p-cancelable@^3.0.0:
version "3.0.0"
@@ -12513,9 +12514,9 @@ proto-list@~1.2.1:
integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
version "2.0.8"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.8.tgz#624e760fd8ab06b1b8320c2f8506d4422cf8e932"
integrity sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==
dependencies:
forwarded "0.2.0"
ipaddr.js "1.9.1"
@@ -13665,9 +13666,9 @@ shebang-regex@^3.0.0:
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.8.4:
version "1.9.0"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.9.0.tgz#e108b1a136586d5964edb3300016d4bedba0fe57"
integrity sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==
version "1.12.0"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.12.0.tgz#e98fdfd2e921d8a58ae2820f4fc0f75c39f09675"
integrity sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==
shelljs@^0.8.5:
version "0.8.5"
@@ -13841,9 +13842,9 @@ sort-css-media-queries@2.2.0:
integrity sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
version "1.2.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.2.tgz#79fb88d97535a21cb27162cb05d211b67acea4ab"
integrity sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==
source-map-support@~0.5.20:
version "0.5.21"

View File

@@ -42,7 +42,7 @@ dependencies = [
# ``google-auth`` 2.53+ dropped it, so Superset must declare it
# explicitly to keep fresh ``pip install apache-superset`` working
# without the ``base.txt`` lock file (#40962).
"cachetools>=7.1.8, <8",
"cachetools>=7.2.0, <8",
"celery>=5.6.3, <6.0.0",
"click>=8.5.0",
"click-option-group",
@@ -71,12 +71,12 @@ dependencies = [
"gunicorn>=26.0.0, <27; sys_platform != 'win32'",
"hashids>=1.3.1, <2",
# holidays>=0.45 required for security fix
"holidays>=0.104, <1",
"holidays>=0.105, <1",
"humanize",
"isodate",
"jsonpath-ng>=1.8.0, <2",
"Mako>=1.4.1",
"markdown>=3.10.3",
"markdown>=3.11",
# marshmallow 4 compatibility: see superset/marshmallow_compatibility.py for a
# Flask-AppBuilder workaround. Tracking issue:
# https://github.com/apache/superset/issues/33162
@@ -94,7 +94,6 @@ dependencies = [
"parsedatetime",
"paramiko>=3.4.0, <4.0", # 4.0 removed DSSKey, still referenced by sshtunnel
"Pillow>=12.3.0, <13", # raise floor to match resolved pin; closes SCA false-positive on 11.x-range CVEs already fixed in 12.3.0
"polyline>=2.0.4, <3.0",
"pydantic>=2.13.5",
"pyparsing>=3.3.2, <4",
"python-dateutil",
@@ -116,7 +115,7 @@ dependencies = [
# Dialect-specific gaps/bugs against this pin are worked around in
# superset/sql/dialects/ (e.g. starrocks.py); check there for anything
# that can be cleaned up when bumping
"sqlglot>=30.18.0, <31", # 30.16.0 adds Trino inline UDF IF/CASE routine statement parsing
"sqlglot>=30.19.0, <31", # 30.16.0 adds Trino inline UDF IF/CASE routine statement parsing
# newer pandas needs 0.9+
"tabulate>=0.10.0, <1.0",
"typing-extensions>=4.16.0, <5",
@@ -143,7 +142,7 @@ bigquery = [
"sqlalchemy-bigquery>=1.17.2",
"google-cloud-bigquery>=3.45.2",
]
clickhouse = ["clickhouse-connect>=1.8.0, <2.0"]
clickhouse = ["clickhouse-connect>=1.9.0, <2.0"]
# The `cockroachdb` PyPI package (last released 2021) is abandoned and its
# SQLAlchemy dialect cannot even import under SQLAlchemy 2.0 (it references
# sqlalchemy.dialects.postgresql.psycopg2.PGCompiler_psycopg2, removed in
@@ -162,7 +161,7 @@ crate = ["sqlalchemy-cratedb>=0.43.1, <1"]
d1 = ["sqlalchemy-d1>=0.2.0, <1"]
databend = ["databend-sqlalchemy>=0.5.5, <1.0"]
databricks = [
"databricks-sql-connector>=4.5.0, <4.6.0",
"databricks-sql-connector>=4.6.0, <4.7.0",
"databricks-sqlalchemy>=2.0.1, <3",
]
datafusion = ["flightsql-dbapi>=0.2.2, <0.3"]
@@ -199,7 +198,7 @@ excel-export = ["boto3"]
# superset.utils.gcs, so this extra is an alternative to excel-export, not an
# addition to it -- pick whichever matches your bucket's provider.
# >=1.37 for ``Blob.open``, which superset.utils.gcs streams downloads through.
excel-export-gcs = ["google-cloud-storage>=1.37"]
excel-export-gcs = ["google-cloud-storage>=3.15.0"]
fastmcp = [
"fastmcp>=3.4.7,<4.0",
# fastmcp 3.x itself requires mcp<2.0; the upper bound here was looser
@@ -319,7 +318,17 @@ development = [
"pre-commit",
"progress>=1.6.1,<2",
"psutil",
"pyfakefs",
# 5.3.5 (the previously resolved version) predates pyfakefs's own Python
# 3.13 support: under 3.13 it leaves os.chdir patched to the fake
# filesystem after the `fs` fixture is undone, so the chdir back to the
# rootdir in pytest-cov's own teardown aborts the whole session with an
# INTERNALERROR. 5.6.0 added preliminary 3.13 support, which is what stops
# that crash, and 5.7.0 made 3.13 support official. 5.7.4 is the floor
# rather than 5.7.0 because it also fixes module/session-scoped fixtures
# under 3.13 (pyfakefs #1101), and the `fs` tests in
# tests/integration_tests/cli_tests.py -- the only consumers of that
# fixture -- run alongside session- and module-scoped fixtures.
"pyfakefs>=5.7.4",
"pyinstrument>=5.1.3,<6",
"pylint",
"pytest<10.0.0", # cap below the next major until validated; the earlier <8 pin (current_app proxy timing) no longer reproduces
@@ -344,7 +353,7 @@ combine_as_imports = true
include_trailing_comma = true
line_length = 88
known_first_party = "superset, apache-superset-core, apache-superset-extensions-cli"
known_third_party = "alembic, apispec, backoff, celery, click, colorama, cron_descriptor, croniter, cryptography, dateutil, deprecation, flask, flask_appbuilder, flask_babel, flask_caching, flask_compress, flask_jwt_extended, flask_login, flask_migrate, flask_sqlalchemy, flask_talisman, flask_testing, flask_wtf, freezegun, geohash, geopy, holidays, humanize, isodate, jinja2, jwt, markdown, markupsafe, marshmallow, marshmallow-union, msgpack, nh3, numpy, pandas, parameterized, parsedatetime, polyline, rison, progress, pyarrow, sqlalchemy_bigquery, pyhive, pyparsing, pytest, pytest_mock, pytz, redis, requests, setuptools, shillelagh, simplejson, slack, sqlalchemy, sqlalchemy_utils, syntaqlite, typing_extensions, urllib3, werkzeug, wtforms, wtforms_json, yaml"
known_third_party = "alembic, apispec, backoff, celery, click, colorama, cron_descriptor, croniter, cryptography, dateutil, deprecation, flask, flask_appbuilder, flask_babel, flask_caching, flask_compress, flask_jwt_extended, flask_login, flask_migrate, flask_sqlalchemy, flask_talisman, flask_testing, flask_wtf, freezegun, geohash, geopy, holidays, humanize, isodate, jinja2, jwt, markdown, markupsafe, marshmallow, marshmallow-union, msgpack, nh3, numpy, pandas, parameterized, parsedatetime, rison, progress, pyarrow, sqlalchemy_bigquery, pyhive, pyparsing, pytest, pytest_mock, pytz, redis, requests, setuptools, shillelagh, simplejson, slack, sqlalchemy, sqlalchemy_utils, syntaqlite, typing_extensions, urllib3, werkzeug, wtforms, wtforms_json, yaml"
multi_line_output = 3
order_by_type = false
@@ -395,14 +404,16 @@ module = [
]
warn_unused_ignores = false
# The mirror case: superset-core declares SqlaTable's id/uuid/perm as plain
# value types, so column expressions like SqlaTable.uuid.in_() only need a
# type: ignore when superset-core's sources are part of the same mypy run
# (full-repo lint) and not when only changed files are checked.
# The mirror case: superset-core declares SqlaTable's id/uuid/perm, and
# Chart's uuid, as plain value types, so column expressions like
# SqlaTable.uuid.in_() and Slice.uuid.in_() only need a type: ignore when
# superset-core's sources are part of the same mypy run (full-repo lint)
# and not when only changed files are checked.
[[tool.mypy.overrides]]
module = [
"superset.mcp_service.dataset_scope",
"tests.unit_tests.mcp_service.dataset.tool.test_dataset_tools",
"superset.commands.chart.importers.v1",
]
warn_unused_ignores = false
@@ -580,10 +591,6 @@ authorized_licenses = [
]
[tool.liccheck.authorized_packages]
# --------------------------------------------------------------
# These are ok, checked manually
# Seems ok, might need legal review
# https://github.com/urschrei/pypolyline/blob/master/LICENSE.md
polyline = "2"
# NumPy 2.x reports a combined SPDX license expression covering vendored
# code (BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0), all of which
# are permissive OSI-approved licenses; the package itself remains

View File

@@ -44,7 +44,7 @@ cachelib==0.17.0
# via
# flask-caching
# flask-session
cachetools==7.1.8
cachetools==7.2.0
# via apache-superset (pyproject.toml)
cattrs==25.1.1
# via requests-cache
@@ -172,7 +172,7 @@ gunicorn==26.2.0
# via apache-superset (pyproject.toml)
hashids==1.3.1
# via apache-superset (pyproject.toml)
holidays==0.104
holidays==0.105
# via apache-superset (pyproject.toml)
humanize==4.12.3
# via apache-superset (pyproject.toml)
@@ -212,7 +212,7 @@ mako==1.4.1
# -r requirements/base.in
# apache-superset (pyproject.toml)
# alembic
markdown==3.10.3
markdown==3.11
# via apache-superset (pyproject.toml)
markdown-it-py==3.0.0
# via rich
@@ -279,8 +279,6 @@ pillow==12.3.0
# via apache-superset (pyproject.toml)
platformdirs==4.3.8
# via requests-cache
polyline==2.0.4
# via apache-superset (pyproject.toml)
prison==0.2.1
# via flask-appbuilder
prompt-toolkit==3.0.51
@@ -397,7 +395,7 @@ sqlalchemy-utils==0.42.1
# apache-superset (pyproject.toml)
# apache-superset-core
# flask-appbuilder
sqlglot==30.18.0
sqlglot==30.21.0
# via
# apache-superset (pyproject.toml)
# apache-superset-core

View File

@@ -112,7 +112,7 @@ cachelib==0.17.0
# -c requirements/base-constraint.txt
# flask-caching
# flask-session
cachetools==7.1.8
cachetools==7.2.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -176,7 +176,7 @@ click-repl==0.3.0
# via
# -c requirements/base-constraint.txt
# celery
clickhouse-connect==1.8.0
clickhouse-connect==1.9.0
# via apache-superset
cmdstanpy==1.1.0
# via prophet
@@ -451,7 +451,7 @@ hashids==1.3.1
# via
# -c requirements/base-constraint.txt
# apache-superset
holidays==0.104
holidays==0.105
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -568,7 +568,7 @@ mako==1.4.1
# -c requirements/base-constraint.txt
# alembic
# apache-superset
markdown==3.10.3
markdown==3.11
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -742,10 +742,6 @@ pluggy==1.5.0
# via pytest
polib==1.2.0
# via apache-superset
polyline==2.0.4
# via
# -c requirements/base-constraint.txt
# apache-superset
pre-commit==4.1.0
# via apache-superset
prison==0.2.1
@@ -824,7 +820,7 @@ pydata-google-auth==1.9.0
# via pandas-gbq
pydruid==0.6.9
# via apache-superset
pyfakefs==5.3.5
pyfakefs==6.2.0
# via apache-superset
pygeohash==3.2.2
# via
@@ -1088,7 +1084,7 @@ sqlalchemy-utils==0.42.1
# apache-superset
# apache-superset-core
# flask-appbuilder
sqlglot==30.18.0
sqlglot==30.21.0
# via
# -c requirements/base-constraint.txt
# apache-superset

View File

@@ -52,9 +52,16 @@ PATTERNS = {
r"^requirements/.+\.txt",
r"^pyproject\.toml",
r"^.pylintrc",
# See the note in "frontend": setup-backend/change-detector are this
# group's own setup.
r"^\.github/actions/",
],
"frontend": [
r"^\.github/workflows/.*(bashlib|frontend|e2e)",
# Composite actions are shared setup, matched by nothing else. Listed
# under "python" too -- setup-backend is a backend action, so gating it
# on "frontend" alone would skip the Python jobs whose setup changed.
r"^\.github/actions/",
r"^superset-frontend/",
],
"docker": [
@@ -74,6 +81,54 @@ PATTERNS = {
}
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
# Maps each PATTERNS group to the CodeQL language(s) its files are written
# in, by file extension rather than directory. PATTERNS above groups files by
# project area (e.g. "frontend" means "under superset-frontend/"), which is
# right for gating CI jobs by area but wrong for picking which CodeQL
# languages to scan: a .js file outside superset-frontend/ (e.g. under
# scripts/ or superset/mcp_service/) is grouped under "python" by PATTERNS,
# so a consumer that reused the "frontend" group as a stand-in for
# "javascript changed" would silently skip scanning it. This reuses the same
# group names as PATTERNS rather than introducing new ones, so a group's
# language(s) can be looked up directly instead of via a separate mapping.
# See https://github.com/apache/superset/issues/44822.
GROUP_LANGUAGE_EXTENSIONS: dict[str, dict[str, str]] = {
"python": {".py": "python"},
"frontend": {
".js": "javascript",
".jsx": "javascript",
".ts": "javascript",
".tsx": "javascript",
".mjs": "javascript",
".cjs": "javascript",
},
}
# Flattened for a direct extension -> language lookup, independent of which
# PATTERNS group (i.e. directory) the file lives in.
LANGUAGE_EXTENSIONS: dict[str, str] = {
ext: lang
for extensions in GROUP_LANGUAGE_EXTENSIONS.values()
for ext, lang in extensions.items()
}
def detect_languages(files: Optional[List[str]]) -> List[str]:
"""Returns the CodeQL languages actually touched, by file extension.
Unlike the PATTERNS groups above, this ignores which directory a file
lives in: a .js file is "javascript" whether it's under
superset-frontend/ or not. `files is None` (workflow_dispatch/schedule)
means "assume everything changed", so every known language is returned.
"""
if files is None:
return sorted(set(LANGUAGE_EXTENSIONS.values()))
languages = set()
for file in files:
_, ext = os.path.splitext(file)
if lang := LANGUAGE_EXTENSIONS.get(ext):
languages.add(lang)
return sorted(languages)
def _is_rate_limited(err: HTTPError) -> bool:
"""Whether a 403 is GitHub throttling rather than a missing token scope."""
@@ -217,6 +272,16 @@ def main(event_type: str, sha: str, repo: str) -> None:
files, patterns_compiled
)
# The 100-file API cap below treats a push/PR touching that many files as
# "everything changed" for the PATTERNS groups; language detection
# honors the same assumption so a consumer combining both outputs never
# sees a language silently excluded by the cap.
languages = (
sorted(set(LANGUAGE_EXTENSIONS.values()))
if files is not None and len(files) >= 99
else detect_languages(files)
)
# Output results
output_path = os.getenv("GITHUB_OUTPUT") or "/tmp/GITHUB_OUTPUT.txt" # noqa: S108
with open(output_path, "a") as f:
@@ -227,6 +292,8 @@ def main(event_type: str, sha: str, repo: str) -> None:
if changed or (files is not None and len(files) >= 99):
print(f"{check}=true", file=f)
print(f"Triggering group: {check}")
print(f"languages={json.dumps(languages)}", file=f)
print(f"Languages detected: {languages}")
def get_git_sha() -> str:

View File

@@ -1,167 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import argparse
import os
import subprocess
from datetime import datetime
XVFB_PRE_CMD = "xvfb-run --auto-servernum --server-args='-screen 0, 1280x1024x24' "
REPO = os.getenv("GITHUB_REPOSITORY") or "apache/superset"
GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME") or "push"
CYPRESS_RECORD_KEY = os.getenv("CYPRESS_RECORD_KEY") or ""
def generate_build_id() -> str:
"""Generates a build ID based on the current timestamp."""
now = datetime.now()
rounded_minute = now.minute - (now.minute % 20)
rounded_time = now.replace(minute=rounded_minute, second=0, microsecond=0)
return (os.getenv("GITHUB_SHA") or "DUMMY")[:8] + rounded_time.strftime(
"%Y%m%d%H%M"
)
def run_cypress_for_test_file(
test_file: str, retries: int, use_dashboard: bool, group: str, dry_run: bool, i: int
) -> int:
"""Runs Cypress for a single test file and retries upon failure."""
cypress_cmd = "./node_modules/.bin/cypress run"
os.environ["TERM"] = "xterm"
os.environ["ELECTRON_DISABLE_GPU"] = "true"
build_id = generate_build_id()
browser = os.getenv("CYPRESS_BROWSER", "chrome")
chrome_flags = "--disable-dev-shm-usage"
for attempt in range(retries):
# Create Cypress command for a single test file
cmd: str = ""
if use_dashboard:
# If/when we want to use cypress' dashboard feature to record the run
group_id = f"matrix{group}-file{i}-{attempt}"
cmd = (
f"{XVFB_PRE_CMD} "
f'{cypress_cmd} --spec "{test_file}" '
f"--config numTestsKeptInMemory=0 "
f"--browser {browser} "
f"--record --group {group_id} --tag {REPO},{GITHUB_EVENT_NAME} "
f"--ci-build-id {build_id} "
f"-- {chrome_flags}"
)
else:
os.environ.pop("CYPRESS_RECORD_KEY", None)
cmd = (
f"{XVFB_PRE_CMD} "
f"{cypress_cmd} "
f"--browser {browser} "
f"--config numTestsKeptInMemory=0 "
f'--spec "{test_file}" '
f"-- {chrome_flags}"
)
print(f"RUN: {cmd} (Attempt {attempt + 1}/{retries})")
if dry_run:
# Print the command instead of executing it
print(f"DRY RUN: {cmd}")
return 0
process = subprocess.Popen( # noqa: S602
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
)
# Stream stdout in real-time
if process.stdout:
for stdout_line in iter(process.stdout.readline, ""):
print(stdout_line, end="")
process.wait()
if process.returncode == 0:
print(f"Test {test_file} succeeded on attempt {attempt + 1}")
return 0
else:
print(f"Test {test_file} failed on attempt {attempt + 1}")
print(f"Test {test_file} failed after {retries} retries.")
return process.returncode
def main() -> None:
parser = argparse.ArgumentParser(
description="Run Cypress tests with retries per test file"
)
parser.add_argument(
"--use-dashboard",
action="store_true",
help="Use Cypress Dashboard for parallelization",
)
parser.add_argument(
"--filter", type=str, required=False, default=None, help="Filter to test"
)
parser.add_argument("--group", type=str, default="Default", help="Group name")
parser.add_argument(
"--retries", type=int, default=3, help="Number of retries per test file"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print the command instead of executing it",
)
args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__))
cypress_base_path = "superset-frontend/cypress-base/"
cypress_base_full_path = os.path.join(script_dir, "../", cypress_base_path)
cypress_tests_path = os.path.join(cypress_base_full_path, "cypress/e2e")
test_files = []
file_count = 0
skipped_count = 0
for root, _, files in os.walk(cypress_tests_path):
for file in files:
if file.endswith("test.ts") or file.endswith("test.js"):
# Skip files prefixed with _skip. (excluded by excludeSpecPattern)
if file.startswith("_skip."):
skipped_count += 1
continue
file_count += 1
test_files.append(
os.path.join(root, file).replace(cypress_base_full_path, "")
)
print(f"Found {file_count} test files ({skipped_count} skipped).")
# Sort test files for deterministic, readable run order.
spec_list = sorted(test_files)
# Run each test file independently with retry logic or dry-run
processed_file_count: int = 0
for i, test_file in enumerate(spec_list):
result = run_cypress_for_test_file(
test_file, args.retries, args.use_dashboard, args.group, args.dry_run, i
)
if result != 0:
print(f"Exiting due to failure in {test_file}")
exit(result)
processed_file_count += 1
print(f"Ran {processed_file_count} test files successfully.")
if __name__ == "__main__":
main()

View File

@@ -98,7 +98,6 @@ BASE_NGINX=80
BASE_SUPERSET=8088
BASE_NODE=9000
BASE_WEBSOCKET=8080
BASE_CYPRESS=8081
BASE_DATABASE=5432
BASE_REDIS=6379
@@ -108,7 +107,6 @@ find_and_claim_port $BASE_NGINX NGINX_PORT
find_and_claim_port $BASE_SUPERSET SUPERSET_PORT
find_and_claim_port $BASE_NODE NODE_PORT
find_and_claim_port $BASE_WEBSOCKET WEBSOCKET_PORT
find_and_claim_port $BASE_CYPRESS CYPRESS_PORT
find_and_claim_port $BASE_DATABASE DATABASE_PORT
find_and_claim_port $BASE_REDIS REDIS_PORT
@@ -144,7 +142,6 @@ export NGINX_PORT
export SUPERSET_PORT
export NODE_PORT
export WEBSOCKET_PORT
export CYPRESS_PORT
export DATABASE_PORT
export REDIS_PORT
@@ -188,7 +185,6 @@ case "${1:-}" in
echo "export SUPERSET_PORT=$SUPERSET_PORT"
echo "export NODE_PORT=$NODE_PORT"
echo "export WEBSOCKET_PORT=$WEBSOCKET_PORT"
echo "export CYPRESS_PORT=$CYPRESS_PORT"
echo "export DATABASE_PORT=$DATABASE_PORT"
echo "export REDIS_PORT=$REDIS_PORT"
exit 0

View File

@@ -49,8 +49,14 @@ pybabel extract \
--project=Superset \
-k _ -k __ -k t -k tn:1,2 -k tct .
# Normalize .pot file
msgcat --sort-by-msgid --no-wrap --no-location superset/translations/messages.pot -o superset/translations/messages.pot
# Normalize the .pot: sort by msgid, keep each message on one line, drop source
# locations. msgcat's sort flag is `--sort-output`; the `--sort-by-msgid`
# spelling used here before is not a msgcat option, and gettext rejects the call
# with `unrecognized option` (verified on 0.23.2 and 1.0). With no `set -e` in
# this script that failure was non-fatal, so the step never ran and the template
# was published unnormalized. `|| exit 1` makes a failed normalization stop the
# script instead.
msgcat --sort-output --no-wrap --no-location superset/translations/messages.pot -o superset/translations/messages.pot || exit 1
cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \
&& mv messages.pot.tmp superset/translations/messages.pot
@@ -80,6 +86,9 @@ pybabel update \
--no-fuzzy-matching
# Chop off last blankline from po/pot files, see https://github.com/python-babel/babel/issues/799
# Only a blank last line is removed: the .pot comes out of msgcat above, which
# writes no trailing blank line, so an unconditional delete would drop the
# last entry's msgstr and leave a template gettext rejects.
for file in $( find superset/translations/** );
do
extension=${file##*.}
@@ -87,7 +96,7 @@ do
if [ $extension == "po" ] || [ $extension == "pot" ]
then
mv $file $file.tmp
sed "$ d" $file.tmp > $file
sed '${/^$/d;}' $file.tmp > $file
rm $file.tmp
fi
done

View File

@@ -71,6 +71,8 @@ class QueryOptions:
# Basic options
catalog: str | None = None
schema: str | None = None
# Cap the last statement's outer SQL limit without increasing a smaller one.
# None leaves SQL limits unchanged; explicit caps also respect SQL_MAX_ROW.
limit: int | None = None
timeout_seconds: int | None = None
@@ -100,6 +102,7 @@ class StatementResult:
data: pd.DataFrame | None = None
row_count: int = 0
execution_time_ms: float | None = None
truncated: bool = False # Rows omitted by the fetch cap, beyond executed_sql
@dataclass

View File

@@ -236,3 +236,4 @@ class SemanticQuery:
limit: int | None = None
offset: int | None = None
group_limit: GroupLimit | None = None
selection_identity_version: str | None = None

View File

@@ -50,6 +50,20 @@ class SemanticView(ABC):
# consumers can rely on the attribute existing and degrade to the
# conservative (Saved-only) picker for views that declare nothing.
features: frozenset[SemanticViewFeature] = frozenset()
selection_identity_version: str | None = None
def validate_selection_version(self, version: object) -> None:
"""Reject selections made under a different member identity contract."""
if (
self.selection_identity_version is not None
and version != self.selection_identity_version
):
raise ValueError(
"Saved semantic selections use an older identity format. "
"Reset the chart selections, explicitly reselect its metrics "
"and dimensions, and save the chart. Display titles cannot be "
"automatically mapped to member IDs."
)
# Implementations must expose a display name for the view.
# Declared here as a type annotation (not abstract) so that existing

View File

@@ -17,12 +17,12 @@
"@babel/core": "^8.0.6",
"@babel/preset-env": "^8.0.6",
"@babel/preset-typescript": "^8.0.1",
"@types/node": "^26.6.2",
"@types/node": "^26.6.3",
"babel-loader": "^10.1.1",
"jsdom": "^30.1.1",
"tscw-config": "^1.1.2",
"typescript": "^7.0.2",
"vitest": "^5.0.1",
"vitest": "^5.0.2",
"webpack": "^5.111.1",
"webpack-cli": "^7.2.3"
}
@@ -1846,9 +1846,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.149.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz",
"integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==",
"version": "0.151.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz",
"integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -1857,9 +1857,9 @@
}
},
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz",
"integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz",
"integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==",
"cpu": [
"arm"
],
@@ -1875,9 +1875,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz",
"integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz",
"integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==",
"cpu": [
"arm64"
],
@@ -1893,9 +1893,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz",
"integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz",
"integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==",
"cpu": [
"arm64"
],
@@ -1911,9 +1911,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz",
"integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz",
"integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==",
"cpu": [
"x64"
],
@@ -1929,9 +1929,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz",
"integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz",
"integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==",
"cpu": [
"x64"
],
@@ -1947,9 +1947,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz",
"integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz",
"integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==",
"cpu": [
"arm"
],
@@ -1965,9 +1965,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz",
"integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz",
"integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==",
"cpu": [
"arm64"
],
@@ -1986,9 +1986,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz",
"integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz",
"integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==",
"cpu": [
"arm64"
],
@@ -2007,9 +2007,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz",
"integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz",
"integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==",
"cpu": [
"ppc64"
],
@@ -2028,9 +2028,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz",
"integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz",
"integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==",
"cpu": [
"s390x"
],
@@ -2049,9 +2049,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz",
"integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz",
"integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==",
"cpu": [
"x64"
],
@@ -2070,9 +2070,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz",
"integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz",
"integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==",
"cpu": [
"x64"
],
@@ -2091,9 +2091,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz",
"integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz",
"integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==",
"cpu": [
"arm64"
],
@@ -2109,9 +2109,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz",
"integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz",
"integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==",
"cpu": [
"arm64"
],
@@ -2127,9 +2127,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz",
"integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz",
"integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==",
"cpu": [
"x64"
],
@@ -2205,9 +2205,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz",
"integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==",
"version": "26.6.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz",
"integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2555,14 +2555,14 @@
}
},
"node_modules/@vitest/mocker": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz",
"integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==",
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz",
"integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.31",
"@vitest/spy": "5.0.1",
"@vitest/spy": "5.0.2",
"estree-walker": "^3.0.3",
"magic-string": "^1.2.3"
},
@@ -2583,9 +2583,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz",
"integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==",
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz",
"integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==",
"dev": true,
"license": "MIT",
"funding": {
@@ -3977,9 +3977,9 @@
}
},
"node_modules/magic-string": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz",
"integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4510,14 +4510,14 @@
}
},
"node_modules/rolldown": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz",
"integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==",
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz",
"integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@oxc-project/types": "=0.149.0",
"@oxc-project/types": "=0.151.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -4527,21 +4527,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm-eabi": "1.2.8",
"@rolldown/binding-android-arm64": "1.2.8",
"@rolldown/binding-darwin-arm64": "1.2.8",
"@rolldown/binding-darwin-x64": "1.2.8",
"@rolldown/binding-freebsd-x64": "1.2.8",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.8",
"@rolldown/binding-linux-arm64-gnu": "1.2.8",
"@rolldown/binding-linux-arm64-musl": "1.2.8",
"@rolldown/binding-linux-ppc64-gnu": "1.2.8",
"@rolldown/binding-linux-s390x-gnu": "1.2.8",
"@rolldown/binding-linux-x64-gnu": "1.2.8",
"@rolldown/binding-linux-x64-musl": "1.2.8",
"@rolldown/binding-openharmony-arm64": "1.2.8",
"@rolldown/binding-win32-arm64-msvc": "1.2.8",
"@rolldown/binding-win32-x64-msvc": "1.2.8"
"@rolldown/binding-android-arm-eabi": "1.2.11",
"@rolldown/binding-android-arm64": "1.2.11",
"@rolldown/binding-darwin-arm64": "1.2.11",
"@rolldown/binding-darwin-x64": "1.2.11",
"@rolldown/binding-freebsd-x64": "1.2.11",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.11",
"@rolldown/binding-linux-arm64-gnu": "1.2.11",
"@rolldown/binding-linux-arm64-musl": "1.2.11",
"@rolldown/binding-linux-ppc64-gnu": "1.2.11",
"@rolldown/binding-linux-s390x-gnu": "1.2.11",
"@rolldown/binding-linux-x64-gnu": "1.2.11",
"@rolldown/binding-linux-x64-musl": "1.2.11",
"@rolldown/binding-openharmony-arm64": "1.2.11",
"@rolldown/binding-win32-arm64-msvc": "1.2.11",
"@rolldown/binding-win32-x64-msvc": "1.2.11"
}
},
"node_modules/saxes": {
@@ -4626,13 +4626,6 @@
"node": ">=8"
}
},
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true,
"license": "ISC"
},
"node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
@@ -4657,9 +4650,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -4677,13 +4670,6 @@
"source-map": "^0.6.0"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true,
"license": "MIT"
},
"node_modules/std-env": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
@@ -5013,9 +4999,9 @@
}
},
"node_modules/vite": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz",
"integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==",
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz",
"integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -5023,7 +5009,7 @@
"lightningcss": "^1.33.0",
"picomatch": "^4.0.7",
"postcss": "^8.5.28",
"rolldown": "~1.2.6",
"rolldown": "~1.2.9",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -5092,14 +5078,14 @@
}
},
"node_modules/vitest": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz",
"integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==",
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz",
"integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/mocker": "5.0.1",
"@vitest/mocker": "5.0.2",
"chai": "^6.2.2",
"es-module-lexer": "^2.3.2",
"expect-type": "^1.4.0",
@@ -5107,10 +5093,10 @@
"obug": "^2.1.4",
"picomatch": "^4.0.7",
"std-env": "^4.2.0",
"tinybench": "6.1.4",
"tinyexec": "1.3.0",
"tinybench": "^6.1.4",
"tinyexec": "^1.3.0",
"tinyglobby": "^0.2.17",
"why-is-node-running": "^2.3.0"
"why-is-node-running": "^3.2.1"
},
"bin": {
"vitest": "vitest.mjs"
@@ -5125,12 +5111,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "5.0.1",
"@vitest/browser-preview": "5.0.1",
"@vitest/browser-playwright": "5.0.2",
"@vitest/browser-preview": "5.0.2",
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
"@vitest/coverage-istanbul": "5.0.1",
"@vitest/coverage-v8": "5.0.1",
"@vitest/ui": "5.0.1",
"@vitest/coverage-istanbul": "5.0.2",
"@vitest/coverage-v8": "5.0.2",
"@vitest/ui": "5.0.2",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
@@ -5370,20 +5356,16 @@
}
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz",
"integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==",
"dev": true,
"license": "MIT",
"dependencies": {
"siginfo": "^2.0.0",
"stackback": "0.0.2"
},
"bin": {
"why-is-node-running": "cli.js"
},
"engines": {
"node": ">=8"
"node": ">=20.11"
}
},
"node_modules/wildcard": {

View File

@@ -41,12 +41,12 @@
"@babel/core": "^8.0.6",
"@babel/preset-env": "^8.0.6",
"@babel/preset-typescript": "^8.0.1",
"@types/node": "^26.6.2",
"@types/node": "^26.6.3",
"babel-loader": "^10.1.1",
"jsdom": "^30.1.1",
"tscw-config": "^1.1.2",
"typescript": "^7.0.2",
"vitest": "^5.0.1",
"vitest": "^5.0.2",
"webpack": "^5.111.1",
"webpack-cli": "^7.2.3"
},

View File

@@ -0,0 +1,89 @@
<!--
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
-->
# Embedded SDK test rig
A host app and a stand-in for Superset's embedded page, on two origins, so the
parts of the SDK that only exist between two documents can be exercised for
real: the MessageChannel handshake, what survives a navigation the dashboard
makes on its own, and how many times the host's guest-token endpoint is asked
for a token.
The unit tests mock `MessageChannel` and `Switchboard`. This rig mocks
neither — it loads the built UMD bundle in Chromium and talks to a page that
speaks the same handshake as `superset-frontend/src/embedded/index.tsx`, over
the real `@superset-ui/switchboard`.
## Run it
```bash
cd superset-embedded-sdk
node testrig/drive.mjs # headless, builds the bundle if it is stale
node testrig/drive.mjs --headed # watch it happen
node testrig/drive.mjs --verbose # + the pages' console output
```
Needs a `chromium`, `chromium-browser` or `google-chrome` on `PATH`, or
`CHROMIUM_PATH` pointing at one. No npm dependencies: the driver speaks the
DevTools protocol over Node's built-in WebSocket.
Exit code is 0 only if every check passed.
## Poke at it by hand
```bash
node testrig/server.mjs # then open http://localhost:8100
```
The host app has buttons for embedding, pushing a theme, calling
`getActiveTabs()` and unmounting; the dashboard page has links that navigate it
the way a tab or a link in a Markdown chart would. The right-hand pane logs
what the host saw, and the counter under the frame is how many guest tokens the
host's endpoint has minted.
`?ttl=15` on the host app shortens the guest token's life to 15 seconds, so the
refresh timer fires while you are watching. `?hang=1` makes the dashboard page
answer no `get` at all, which is how the rig produces a call that is still in
flight when the user clicks a link. `?slowfirst=1` holds the very first
`fetchGuestToken()` open until you settle it yourself with
`rigReleaseFirstToken()` or `rigFailFirstToken()` from the console, and
`?failnth=N` makes call N reject — together they are how the rig reaches the
race between the initial fetch and a navigation that happens during it.
## What the pieces are
| file | what it stands for |
| --- | --- |
| `server.mjs` | the host app's backend (`/guest-token`, which counts what it mints) on `:8100`, and the Superset instance on `:8200` |
| `host.html` | the host app: calls `embedDashboard`, drives the public API, records everything into `window.rig` |
| `embedded.html` | Superset's embedded page: the same port-transfer handshake, the same method names, plus links that navigate itself |
| `drive.mjs` | headless Chromium driver and the checks |
`embedded.html` reports what it experienced to the host through an ordinary
`postMessage` to `parent`. That side channel is the rig's own and has nothing
to do with the SDK's MessageChannel — it is how the driver can see what
happened inside a cross-origin frame.
## What it checks
- a first embed renders, on exactly one minted token
- a navigation internal to the dashboard re-authenticates the new document
- the host's theme is re-applied to that document, after its token
- a `get` still in flight when the user navigates is rejected as a
`PortClosedError` rather than left hanging, and so is one caught by `unmount()`
- a navigation that does not land on the embedded page mints no token at all,
and coming back to the dashboard afterwards works
- the refresh timer follows the current document, fires once, and leaves the
document that is gone alone
- an initial token fetch that fails *after* a navigation already recovered the
embed leaves that working dashboard alone, and `embedDashboard` resolves
- a token from a superseded cycle is handed straight to the document in front of
the user when that document has none, rather than waiting out a retry
Run it against `git show HEAD~1:...` of `src/index.ts` to watch the checks fail.

View File

@@ -0,0 +1,655 @@
/*
* 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.
*/
// Drives the rig in headless Chromium over the DevTools protocol, with no
// dependencies beyond a chromium binary. `node drive.mjs [--headed] [--verbose]`
import { spawn, execFileSync } from "node:child_process";
import { mkdtempSync, existsSync, statSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { start, HOST_PORT } from "./server.mjs";
const here = dirname(fileURLToPath(import.meta.url));
const sdkRoot = join(here, "..");
const headed = process.argv.includes("--headed");
const verbose = process.argv.includes("--verbose");
const CHROMIUM =
process.env.CHROMIUM_PATH ||
["chromium", "chromium-browser", "google-chrome", "google-chrome-stable"].find(
(bin) => {
try {
execFileSync("which", [bin], { stdio: "ignore" });
return true;
} catch {
return false;
}
},
);
// ------------------------------------------------------------------ results
const results = [];
function check(name, ok, detail = "") {
results.push({ name, ok, detail });
const mark = ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
console.log(` ${mark} ${name}${detail && !ok ? `\n ${detail}` : ""}`);
}
// Waits for something to become true, and records the wait itself as a check,
// so a run against code that never gets there reports a failure per scenario
// instead of stopping at the first one.
async function expect(name, predicate, timeoutMs = 15_000) {
try {
await waitFor(predicate, name, timeoutMs);
check(name, true);
return true;
} catch (err) {
check(name, false, err.message);
return false;
}
}
// ---------------------------------------------------------------- cdp client
class CDP {
constructor(ws) {
this.ws = ws;
this.nextId = 0;
this.pending = new Map();
this.listeners = [];
ws.addEventListener("message", (event) => {
// Nothing upstream catches a throw from this listener, so a frame that
// does not parse is dropped rather than taking the whole run down.
let msg;
try {
msg = JSON.parse(event.data);
} catch (err) {
if (verbose) console.error(` [cdp] dropped an unparsable frame: ${err.message}`);
return;
}
if (msg.id && this.pending.has(msg.id)) {
const { resolve, reject } = this.pending.get(msg.id);
this.pending.delete(msg.id);
if (msg.error) reject(new Error(JSON.stringify(msg.error)));
else resolve(msg.result);
} else {
this.listeners.forEach((fn) => fn(msg));
}
});
}
send(method, params = {}, sessionId) {
const id = (this.nextId += 1);
const payload = { id, method, params };
if (sessionId) payload.sessionId = sessionId;
this.ws.send(JSON.stringify(payload));
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
setTimeout(() => {
if (this.pending.delete(id)) reject(new Error(`${method} timed out`));
}, 30_000);
});
}
}
// The `stop` of every browser still running, so an interrupted run can stop
// them too.
const runningBrowsers = new Set();
async function launchBrowser() {
const userDataDir = mkdtempSync(join(tmpdir(), "embedded-sdk-rig-"));
const args = [
...(headed ? [] : ["--headless=new"]),
"--remote-debugging-port=0",
`--user-data-dir=${userDataDir}`,
"--no-first-run",
"--no-default-browser-check",
"--disable-gpu",
"--disable-dev-shm-usage",
"about:blank",
];
// In a process group of its own, so the helper processes chromium forks can
// be killed along with it: left alive, they write back into the profile
// after it has been removed.
const child = spawn(CHROMIUM, args, {
stdio: ["ignore", "pipe", "pipe"],
detached: true,
});
const exited = new Promise((resolve) => {
child.once("exit", resolve);
child.once("error", resolve);
});
let stopping;
const stop = () =>
(stopping ??= (async () => {
runningBrowsers.delete(stop);
try {
process.kill(-child.pid, "SIGKILL");
} catch {
// already gone, or never started
}
await exited;
rmSync(userDataDir, { recursive: true, force: true, maxRetries: 3 });
})());
runningBrowsers.add(stop);
// Until `stop` is handed back, nobody else can clean up after this browser.
try {
return { cdp: await connect(child), stop };
} catch (err) {
await stop();
throw err;
}
}
async function connect(child) {
const wsUrl = await new Promise((resolve, reject) => {
let buffered = "";
const onChunk = (chunk) => {
buffered += chunk;
const match = buffered.match(/ws:\/\/\S+/);
if (match) resolve(match[0]);
};
child.stdout.on("data", onChunk);
child.stderr.on("data", onChunk);
child.on("exit", (code) =>
reject(new Error(`chromium exited (${code}) before listening:\n${buffered}`)),
);
child.on("error", (err) =>
reject(new Error(`could not start ${CHROMIUM}: ${err.message}`)),
);
setTimeout(() => reject(new Error("chromium never reported a devtools url")), 20_000);
});
const ws = new WebSocket(wsUrl);
await new Promise((resolve, reject) => {
ws.addEventListener("open", resolve, { once: true });
ws.addEventListener(
"error",
() => reject(new Error(`could not connect to chromium's devtools at ${wsUrl}`)),
{ once: true },
);
});
return new CDP(ws);
}
// ------------------------------------------------------------------- a page
async function openPage(cdp, url) {
const { targetId } = await cdp.send("Target.createTarget", { url: "about:blank" });
const { sessionId } = await cdp.send("Target.attachToTarget", {
targetId,
flatten: true,
});
await cdp.send("Runtime.enable", {}, sessionId);
await cdp.send("Page.enable", {}, sessionId);
if (verbose) {
cdp.listeners.push((msg) => {
if (msg.method === "Runtime.consoleAPICalled" && msg.sessionId === sessionId) {
const text = msg.params.args
.map((a) => a.value ?? a.description ?? a.type)
.join(" ");
console.log(` [page] ${text}`);
}
});
}
const evaluate = async (expression) => {
const r = await cdp.send(
"Runtime.evaluate",
{ expression, awaitPromise: true, returnByValue: true },
sessionId,
);
if (r.exceptionDetails) {
throw new Error(
r.exceptionDetails.exception?.description ||
r.exceptionDetails.text ||
JSON.stringify(r.exceptionDetails),
);
}
return r.result.value;
};
await cdp.send("Page.navigate", { url }, sessionId);
// Wait for the host app's own globals rather than a load event.
await waitFor(
() => evaluate("typeof window.rig === 'object' && !!window.supersetEmbeddedSdk"),
"the host app to load",
);
return { evaluate, sessionId };
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// A rejected predicate counts as "not yet", not as a failure: a navigation that
// destroys the execution context mid-poll makes `evaluate` reject, and that
// navigation is exactly the race this rig exists to reproduce.
async function waitFor(predicate, what, timeoutMs = 15_000, intervalMs = 100) {
const deadline = Date.now() + timeoutMs;
let lastError;
for (;;) {
try {
const value = await predicate();
if (value) return value;
} catch (err) {
lastError = err;
}
if (Date.now() > deadline) {
throw new Error(
`timed out waiting for ${what}${
lastError ? ` (last error: ${lastError.message})` : ""
}`,
);
}
await sleep(intervalMs);
}
}
// ------------------------------------------------------------------ helpers
function makeHelpers(evaluate) {
return {
evaluate,
click: (id) => evaluate(`document.getElementById(${JSON.stringify(id)}).click()`),
events: () => evaluate("window.rig.events"),
errors: () => evaluate("window.rig.errors"),
tokensMinted: () => evaluate("window.rigRefreshStats().then(s => s.tokensMinted)"),
navigateGuest: (to) => evaluate(`window.rigNavigateGuest(${JSON.stringify(to)})`),
embedState: () => evaluate("window.rig.embedState"),
releaseFirstToken: () => evaluate("window.rigReleaseFirstToken()"),
failFirstToken: () => evaluate("window.rigFailFirstToken()"),
heldFirstFetch: () => evaluate("!!window.rig.heldFirstFetch"),
hasIframe: () => evaluate("!!document.querySelector('#mount iframe')"),
getActiveTabs: () => evaluate("window.rigGetActiveTabs()"),
// Events the embedded page reported from a given page of the dashboard.
eventsOn: async (page, name) =>
(await evaluate("window.rig.events")).filter(
(e) => e.page === String(page) && (!name || e.event === name),
),
};
}
// ----------------------------------------------------------------- the runs
async function mainRun(cdp, hostOrigin) {
console.log("\n\x1b[1mmain run\x1b[0m (300s tokens)");
const { evaluate } = await openPage(cdp, `${hostOrigin}/`);
const h = makeHelpers(evaluate);
// -- 1. the ordinary first embed -----------------------------------------
await h.click("embed");
await expect(
"first load: the embedded page renders with a guest token",
async () => (await h.eventsOn(1, "guestToken")).length === 1,
);
check(
"first load: exactly one token minted",
(await h.tokensMinted()) === 1,
`minted ${await h.tokensMinted()}`,
);
// -- 2. the bug this PR is about ------------------------------------------
await evaluate("window.rig.dashboard.setThemeConfig({token:{colorPrimary:'#ff0066'}})");
await evaluate("window.rig.dashboard.setThemeMode('dark')");
await expect(
"first load: the host's theme reaches the document",
async () => (await h.eventsOn(1, "setThemeMode")).length === 1,
);
await h.navigateGuest("/embedded/rig-dashboard?page=2");
await expect(
"internal navigation: the new document is re-authenticated",
async () => (await h.eventsOn(2, "guestToken")).length === 1,
);
check(
"internal navigation: one further token minted, not more",
(await h.tokensMinted()) === 2,
`minted ${await h.tokensMinted()}`,
);
// The finding this round of fixes came from: methods were replayed, state
// was not, so the dashboard came back in the default theme.
await expect(
"internal navigation: the host's theme is re-applied",
async () => (await h.eventsOn(2, "setThemeMode")).length === 1,
);
const page2 = (await h.eventsOn(2)).map((e) => e.event);
check(
"internal navigation: the token arrives before the theme",
page2.indexOf("guestToken") < page2.indexOf("setThemeConfig"),
page2.join(" → "),
);
check(
"internal navigation: the dark theme is actually on the new document",
await evaluate(
"document.querySelector('#mount iframe') && window.rig.events.some(e => e.page==='2' && e.event==='setThemeMode' && e.detail.mode==='dark')",
),
);
// -- 3. a call in flight when the document leaves --------------------------
await h.navigateGuest("/embedded/rig-dashboard?page=3&hang=1");
await expect(
"a page that answers nothing still gets its token",
async () => (await h.eventsOn(3, "guestToken")).length === 1,
);
await h.click("tabs"); // page 3 never answers this
await sleep(300);
await h.navigateGuest("/embedded/rig-dashboard?page=4");
const rejected = await expect(
"a call in flight when the user navigates is rejected, not left hanging",
async () => (await h.errors()).length === 1,
);
if (rejected) {
const [err] = await h.errors();
check(
"…and it is rejected as a PortClosedError",
err.name === "PortClosedError",
JSON.stringify(err),
);
}
// -- 4. a navigation that does not land on the embedded page --------------
const beforeStray = await h.tokensMinted();
await h.navigateGuest("/plain");
// The handshake probe gives the new document 5s to answer.
await sleep(7000);
check(
"a navigation away from the embedded page mints no guest token",
(await h.tokensMinted()) === beforeStray,
`minted ${await h.tokensMinted()}, was ${beforeStray}`,
);
// -- 5. and back again ----------------------------------------------------
// The stray page cannot navigate itself back for us, so the host reloads the
// frame the way a host app would.
await evaluate(
"document.querySelector('#mount iframe').src = '__SUPERSET__/embedded/rig-dashboard?page=5&hang=1'".replace(
"__SUPERSET__",
await evaluate("SUPERSET_ORIGIN"),
),
);
await expect(
"coming back to the embedded page re-authenticates again",
async () => (await h.eventsOn(5, "guestToken")).length === 1,
);
check(
"coming back mints exactly one more token",
(await h.tokensMinted()) === beforeStray + 1,
`minted ${await h.tokensMinted()}, was ${beforeStray}`,
);
// -- 6. unmount -----------------------------------------------------------
// Page 5 answers nothing either, so this call is still in flight.
await h.click("tabs");
await sleep(200);
await evaluate("window.rig.dashboard.unmount()");
if (
await expect(
"unmount rejects what is still in flight",
async () => (await h.errors()).length === 2,
)
) {
const errs = await h.errors();
check(
"…and it too is a PortClosedError",
errs[1].name === "PortClosedError",
JSON.stringify(errs[1]),
);
}
const afterUnmount = await h.tokensMinted();
await sleep(1000);
check(
"nothing is minted after unmount",
(await h.tokensMinted()) === afterUnmount,
);
}
async function refreshRun(cdp, hostOrigin) {
console.log("\n\x1b[1mrefresh run\x1b[0m (15s tokens, so the timer fires during the run)");
await fetch(`${hostOrigin}/reset`);
const { evaluate } = await openPage(cdp, `${hostOrigin}/?ttl=15`);
const h = makeHelpers(evaluate);
await h.click("embed");
await expect(
"refresh run: the first token arrives",
async () => (await h.eventsOn(1, "guestToken")).length === 1,
);
await h.navigateGuest("/embedded/rig-dashboard?page=2");
await expect(
"refresh run: the navigated-to page is authenticated",
async () => (await h.eventsOn(2, "guestToken")).length === 1,
);
const mintedAfterNav = await h.tokensMinted();
// A 15s token refreshes 5s before it expires, so ~10s from when it was
// issued. The point is which port that refresh lands on.
await expect(
"the refresh timer follows the current document",
async () => (await h.eventsOn(2, "guestToken")).length === 2,
20_000,
);
check(
"the old document gets nothing",
(await h.eventsOn(1, "guestToken")).length === 1,
);
check(
"exactly one refresh, so there is only one timer chain",
(await h.tokensMinted()) === mintedAfterNav + 1,
`minted ${await h.tokensMinted()}, was ${mintedAfterNav}`,
);
await evaluate("window.rig.dashboard?.unmount()");
}
// The two directions of the race between the very first token fetch and a
// navigation the dashboard makes while that fetch is still in flight. Neither
// is reachable with a host endpoint that answers promptly, so the rig holds
// the first fetch open and settles it by hand.
async function staleInitialFetchRun(cdp, hostOrigin) {
console.log(
"\n\x1b[1mstale initial fetch run\x1b[0m (the first fetch fails after a navigation already recovered the embed)",
);
await fetch(`${hostOrigin}/reset`);
const { evaluate } = await openPage(cdp, `${hostOrigin}/?slowfirst=1`);
const h = makeHelpers(evaluate);
await h.click("embed");
// The frame is up and page 1 is blank, waiting for a token that the rig is
// sitting on.
await expect("stale initial fetch: the first fetch is in flight", h.heldFirstFetch);
await expect(
"stale initial fetch: the first document is still waiting for a token",
async () => (await h.eventsOn(1, "started")).length === 1,
);
check(
"stale initial fetch: …and has not been given one",
(await h.eventsOn(1, "guestToken")).length === 0,
);
// The user clicks a link before the host's endpoint has answered.
await h.navigateGuest("/embedded/rig-dashboard?page=2");
await expect(
"stale initial fetch: the navigated-to document authenticates on its own",
async () => (await h.eventsOn(2, "guestToken")).length === 1,
);
const mintedWhileWorking = await h.tokensMinted();
// Only now does the original fetch give up. Nobody is waiting for it.
await h.failFirstToken();
await sleep(500);
check(
"stale initial fetch: the working dashboard is not torn down",
await h.hasIframe(),
);
check(
"stale initial fetch: embedDashboard resolves rather than rejecting",
(await h.embedState()) === "resolved",
`embedState = ${await h.embedState()}`,
);
// The surest proof the embed is still live: the port still answers, with the
// navigated-to document's own answer.
const tabs = await h.getActiveTabs();
check(
"stale initial fetch: the port still answers, from the current document",
tabs.ok && tabs.tabs?.[0] === "tab-on-page-2",
JSON.stringify(tabs),
);
check(
"stale initial fetch: the failure mints nothing further",
(await h.tokensMinted()) === mintedWhileWorking,
`minted ${await h.tokensMinted()}, was ${mintedWhileWorking}`,
);
await evaluate("window.rig.dashboard?.unmount()");
}
async function supersededTokenRun(cdp, hostOrigin) {
console.log(
"\n\x1b[1msuperseded token run\x1b[0m (the navigation's own fetch fails, the first one succeeds late)",
);
await fetch(`${hostOrigin}/reset`);
// Call #1 is held; call #2 — the one the navigation triggers — rejects.
const { evaluate } = await openPage(cdp, `${hostOrigin}/?slowfirst=1&failnth=2`);
const h = makeHelpers(evaluate);
await h.click("embed");
await expect("superseded token: the first fetch is in flight", h.heldFirstFetch);
// The first document has to be listening before it can be told to navigate:
// the held fetch starts before the iframe has loaded.
await expect(
"superseded token: the first document is up and waiting for a token",
async () => (await h.eventsOn(1, "started")).length === 1,
);
await h.navigateGuest("/embedded/rig-dashboard?page=2");
// The navigation's own fetch fails, so page 2 is blank and a retry is armed
// ten seconds out.
await expect(
"superseded token: the navigated-to document reaches the handshake",
async () => (await h.eventsOn(2, "started")).length === 1,
);
await expect(
"superseded token: its own fetch failed, so it has no token yet",
async () => (await evaluate("window.rig.tokenFetches")) === 2,
);
check(
"superseded token: …and the document is still blank",
(await h.eventsOn(2, "guestToken")).length === 0,
);
// The first fetch finally answers. Its token is superseded but perfectly
// valid, and the document in front of the user has none.
await h.releaseFirstToken();
// Well inside the 10s retry interval: holding the token back until the failed
// cycle retries is exactly the blank page this is about, so a token that only
// turns up on the retry is not a pass.
await expect(
"superseded token: the superseded token is handed straight to the current document",
async () => (await h.eventsOn(2, "guestToken")).length === 1,
3_000,
);
check(
"superseded token: nothing is sent to the document that is gone",
(await h.eventsOn(1, "guestToken")).length === 0,
);
check(
"superseded token: one token minted, not a second for the retry",
(await h.tokensMinted()) === 1,
`minted ${await h.tokensMinted()}`,
);
check(
"superseded token: embedDashboard resolves",
(await h.embedState()) === "resolved",
`embedState = ${await h.embedState()}`,
);
await evaluate("window.rig.dashboard?.unmount()");
}
// --------------------------------------------------------------------- main
// Everything the bundle is built from, not just the sources: a change to the
// build config or the dependencies is just as able to make it stale.
const BUILD_INPUTS = [
"webpack.config.js",
"babel.config.js",
"tsconfig.json",
"package.json",
"package-lock.json",
];
function buildIfStale() {
const bundle = join(sdkRoot, "bundle", "index.js");
const inputs = [
...readdirSync(join(sdkRoot, "src"), { recursive: true }).map((f) =>
join("src", f),
),
...BUILD_INPUTS,
]
.map((f) => join(sdkRoot, f))
.filter((f) => existsSync(f) && statSync(f).isFile());
const newestInput = Math.max(...inputs.map((f) => statSync(f).mtimeMs));
if (existsSync(bundle) && statSync(bundle).mtimeMs > newestInput) return;
console.log("building the sdk bundle…");
execFileSync("npx", ["webpack", "--mode", "development"], {
cwd: sdkRoot,
stdio: verbose ? "inherit" : "ignore",
});
}
async function main() {
if (!CHROMIUM) {
console.error(
"no chromium found. Install one, or set CHROMIUM_PATH to a chrome binary.",
);
process.exit(2);
}
buildIfStale();
// Both are started inside the `try`, so whichever of them did come up is
// stopped even when the other one fails to.
let server;
let browser;
try {
server = await start();
browser = await launchBrowser();
await mainRun(browser.cdp, server.hostOrigin);
await refreshRun(browser.cdp, server.hostOrigin);
await staleInitialFetchRun(browser.cdp, server.hostOrigin);
await supersededTokenRun(browser.cdp, server.hostOrigin);
} finally {
await browser?.stop();
server?.stop();
}
const failed = results.filter((r) => !r.ok);
console.log(
`\n${results.length - failed.length}/${results.length} checks passed`,
);
process.exit(failed.length ? 1 : 0);
}
// Chromium runs in its own process group, so a Ctrl-C no longer reaches it on
// its own. The servers need nothing: they go with this process.
for (const signal of ["SIGINT", "SIGTERM"]) {
process.once(signal, async () => {
console.error(`\n${signal}: stopping the rig`);
await Promise.all([...runningBrowsers].map((stop) => stop()));
process.exit(130);
});
}
main().catch((err) => {
console.error("\nrig failed:", err);
process.exit(1);
});

View File

@@ -0,0 +1,127 @@
<!doctype html>
<html lang="en">
<!--
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
-->
<head>
<meta charset="utf-8" />
<title>rig: embedded dashboard</title>
<style>
:root { color-scheme: light; --bg: #fff; --fg: #1b1b1b; --muted: #666; }
body.dark { color-scheme: dark; --bg: #16181d; --fg: #e8e8e8; --muted: #9aa; }
body { margin: 0; padding: 20px; font: 14px/1.5 system-ui, sans-serif;
background: var(--bg); color: var(--fg); }
h1 { font-size: 16px; margin: 0 0 4px; }
.muted { color: var(--muted); }
.chart { margin: 16px 0; padding: 16px; border: 1px solid #8884; border-radius: 8px; }
.blank { color: #b00; font-weight: 600; }
a { color: inherit; }
code { font-family: ui-monospace, monospace; }
</style>
</head>
<body>
<h1>Embedded dashboard — <span id="page">page 1</span></h1>
<p class="muted">This page stands in for Superset's embedded page. It speaks
the same handshake as <code>superset-frontend/src/embedded/index.tsx</code>.</p>
<div class="chart">
<div id="content" class="blank">blank: waiting for a guest token…</div>
<div class="muted" id="detail"></div>
</div>
<p>
<!-- A link internal to the dashboard: a tab, a drill-down, a link in a
Markdown chart. It navigates this same iframe. -->
<a id="internal-link" href="#">go to page 2 (link internal to the dashboard)</a><br />
<a id="offsite-link" href="/plain">go to a page that is not the embedded one</a>
</p>
<script type="module">
import Switchboard from "/vendor/switchboard.js";
const params = new URLSearchParams(location.search);
const page = params.get("page") || "1";
document.getElementById("page").textContent = `page ${page}`;
document.getElementById("internal-link").href =
`${location.pathname}?page=${Number.isNaN(+page) ? 2 : +page + 1}` +
(params.get("hang") ? "&hang=1" : "");
// The rig's own side channel, so the driver can see what this document
// experienced. It is nothing to do with the SDK's MessageChannel.
const report = (event, detail) =>
parent.postMessage({ __rig: true, page, event, detail }, "*");
let tokensReceived = 0;
function render(detail) {
const el = document.getElementById("content");
el.className = "";
el.textContent = `rendered with a guest token (page ${page})`;
document.getElementById("detail").textContent = detail;
}
// Mirrors the real page: everything is defined inside the port-transfer
// handler, and `start()` comes last.
window.addEventListener("message", function embeddedPageInitializer(event) {
const port = event.ports?.[0];
if (event.data?.type !== "__embedded_comms__") return;
if (event.data.handshake === "port transfer" && port) {
report("handshake");
Switchboard.init({ port, name: "superset", debug: false });
Switchboard.defineMethod("guestToken", ({ guestToken }) => {
tokensReceived += 1;
const claims = JSON.parse(atob(guestToken.split(".")[1]));
render(`token #${claims.serial}, exp in ${
claims.exp - Math.floor(Date.now() / 1000)
}s, ${tokensReceived} received on this port`);
report("guestToken", { serial: claims.serial, tokensReceived });
});
Switchboard.defineMethod("getActiveTabs", () => {
// `?hang=1` makes this page answer nothing, standing in for a reply
// that is still being computed when the user clicks a link.
if (params.get("hang")) return new Promise(() => {});
return [`tab-on-page-${page}`];
});
Switchboard.defineMethod("getScrollSize", () => {
if (params.get("hang")) return new Promise(() => {});
return { width: 800, height: 600 };
});
Switchboard.defineMethod("setThemeConfig", ({ themeConfig }) => {
report("setThemeConfig", themeConfig);
return { success: true };
});
Switchboard.defineMethod("setThemeMode", ({ mode }) => {
document.body.classList.toggle("dark", mode === "dark");
report("setThemeMode", { mode });
return { success: true };
});
Switchboard.start();
report("started");
}
});
// The rig drives navigation the way the dashboard itself would: this
// document sets its own location. The host has no other way to reach into a
// cross-origin frame, and neither would a real one.
window.addEventListener("message", (event) => {
if (event.data?.__rigCommand === "navigate") {
location.href = event.data.to;
}
});
report("loaded");
</script>
</body>
</html>

View File

@@ -0,0 +1,239 @@
<!doctype html>
<html lang="en">
<!--
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
-->
<head>
<meta charset="utf-8" />
<title>embedded-sdk test rig — host app</title>
<style>
body { margin: 0; font: 14px/1.5 system-ui, sans-serif; display: grid;
grid-template-columns: 1fr 420px; height: 100vh; }
main { padding: 16px; overflow: auto; }
aside { border-left: 1px solid #ddd; padding: 16px; overflow: auto; background: #fafafa; }
h1 { font-size: 16px; margin: 0 0 12px; }
#mount { border: 1px solid #ccc; border-radius: 8px; height: 320px; }
#mount iframe { width: 100%; height: 100%; border: 0; border-radius: 8px; }
button { font: inherit; margin: 0 6px 6px 0; padding: 4px 10px; }
#log { font: 12px/1.45 ui-monospace, monospace; white-space: pre-wrap; }
.t { color: #888; }
.err { color: #b00; }
.ok { color: #070; }
</style>
</head>
<body>
<main>
<h1>Host app</h1>
<div>
<button id="embed">embed</button>
<button id="theme">setThemeMode(dark) + setThemeConfig</button>
<button id="tabs">getActiveTabs()</button>
<button id="unmount">unmount</button>
<button id="clear">clear log</button>
</div>
<div id="mount"></div>
<p class="t">Tokens minted by this host's endpoint: <b id="minted">0</b></p>
</main>
<aside><div id="log"></div></aside>
<script src="/sdk.js"></script>
<script>
const SUPERSET_ORIGIN = "__SUPERSET_ORIGIN__";
// Everything the driver reads.
const rig = (window.rig = {
events: [], // what the embedded page reported over its side channel
log: [], // what the host app saw
errors: [], // rejections the SDK handed back
tokenFetches: 0, // calls the SDK made to fetchGuestToken
dashboard: null,
embedState: "idle", // pending | resolved | rejected
embedError: null,
heldFirstFetch: null,
});
function log(kind, ...parts) {
const line = `${new Date().toISOString().slice(11, 23)} ${kind} ${parts
.map((p) => (typeof p === "string" ? p : JSON.stringify(p)))
.join(" ")}`;
rig.log.push(line);
const el = document.createElement("div");
el.className = kind === "error" ? "err" : kind === "ok" ? "ok" : "";
el.textContent = line;
document.getElementById("log").prepend(el);
}
// The embedded page's side channel (not the SDK's MessageChannel).
window.addEventListener("message", (event) => {
if (!event.data?.__rig) return;
rig.events.push({ ...event.data, at: Date.now() });
log("guest", `page ${event.data.page}`, event.data.event, event.data.detail ?? "");
});
async function refreshStats() {
const stats = await fetch("/stats").then((r) => r.json());
document.getElementById("minted").textContent = stats.tokensMinted;
rig.tokensMinted = stats.tokensMinted;
return stats;
}
window.rigRefreshStats = refreshStats;
// TTL is a query param so a run can exercise the refresh timer in seconds
// rather than minutes.
const search = new URLSearchParams(location.search);
const ttl = search.get("ttl") || "300";
// `?slowfirst=1` holds the very first fetchGuestToken() until the driver
// settles it by hand, which is the only way to have a navigation happen
// while the initial token is still in flight. `?failnth=N` makes call N
// reject, standing in for a host endpoint that is down.
const slowFirst = search.get("slowfirst");
const failNth = Number(search.get("failnth") || 0);
async function mintToken() {
const { token } = await fetch(`/guest-token?ttl=${ttl}`).then((r) => r.json());
refreshStats();
return token;
}
async function fetchGuestToken() {
rig.tokenFetches += 1;
const call = rig.tokenFetches;
log("host", `fetchGuestToken() #${call}`);
if (call === failNth) {
log("error", `fetchGuestToken() #${call} rejects (rig)`);
throw new Error(`rig: host token endpoint is down (call #${call})`);
}
if (slowFirst && call === 1) {
log("host", "fetchGuestToken() #1 held by the rig");
return new Promise((resolve, reject) => {
rig.heldFirstFetch = { resolve, reject };
});
}
return mintToken();
}
// The held first fetch, handed out once so it cannot be settled twice.
function takeHeldFirstFetch(action) {
const held = rig.heldFirstFetch;
if (!held) {
log("error", `${action}: no fetchGuestToken() is being held (needs ?slowfirst=1)`);
return null;
}
rig.heldFirstFetch = null;
return held;
}
// Settle the held first fetch, long after the SDK asked for it.
window.rigReleaseFirstToken = async () => {
const held = takeHeldFirstFetch("rigReleaseFirstToken");
if (!held) return;
try {
held.resolve(await mintToken());
log("host", "released the held fetchGuestToken() #1");
} catch (err) {
// Never leave the SDK waiting on a fetch nobody will settle.
held.reject(err);
log("error", `could not mint the held token, failed fetchGuestToken() #1: ${err.message}`);
}
};
window.rigFailFirstToken = () => {
const held = takeHeldFirstFetch("rigFailFirstToken");
if (!held) return;
held.reject(new Error("rig: host token endpoint is down"));
log("error", "failed the held fetchGuestToken() #1");
};
document.getElementById("embed").onclick = () => {
// Deliberately not awaited: a run needs to watch what happens while
// `embedDashboard` is still pending, and to see how it ends up settling.
rig.embedState = "pending";
supersetEmbeddedSdk
.embedDashboard({
id: "rig-dashboard",
supersetDomain: SUPERSET_ORIGIN,
mountPoint: document.getElementById("mount"),
fetchGuestToken,
debug: true,
dashboardUiConfig: search.get("hang")
? { urlParams: { hang: "1" } }
: undefined,
})
.then(
(dashboard) => {
rig.dashboard = dashboard;
rig.embedState = "resolved";
log("ok", "embedDashboard resolved");
},
(err) => {
rig.embedState = "rejected";
rig.embedError = { name: err.name, message: err.message };
log("error", `embedDashboard rejected: ${err.message}`);
},
);
};
// The buttons are live before `embedDashboard` resolves, and stay live if it
// rejects, so each one checks there is a dashboard to act on.
function hasDashboard(action) {
if (rig.dashboard) return true;
log("error", `${action}: no dashboard yet (embedDashboard is ${rig.embedState})`);
return false;
}
document.getElementById("theme").onclick = () => {
if (!hasDashboard("theme")) return;
rig.dashboard.setThemeConfig({ token: { colorPrimary: "#ff0066" } });
rig.dashboard.setThemeMode("dark");
log("host", "theme pushed");
};
document.getElementById("tabs").onclick = () => {
if (!hasDashboard("getActiveTabs()")) return;
log("host", "getActiveTabs() called");
rig.dashboard.getActiveTabs().then(
(tabs) => log("ok", "getActiveTabs ->", tabs),
(err) => {
rig.errors.push({ name: err.name, message: err.message, at: Date.now() });
log("error", `getActiveTabs rejected: ${err.name}: ${err.message}`);
},
);
};
document.getElementById("unmount").onclick = () => {
if (!hasDashboard("unmount()")) return;
rig.dashboard.unmount();
log("host", "unmount()");
};
document.getElementById("clear").onclick = () => {
document.getElementById("log").textContent = "";
};
// Same call as the button, but handing the outcome back to the driver, so a
// run can tell "the port answered" from "there was nothing to ask".
window.rigGetActiveTabs = () => {
if (!rig.dashboard) return Promise.resolve({ ok: false, name: "NoDashboard" });
return rig.dashboard.getActiveTabs().then(
(tabs) => ({ ok: true, tabs }),
(err) => ({ ok: false, name: err.name, message: err.message }),
);
};
// Ask the embedded page to navigate itself, which is what a tab, a
// drill-down or a link in a Markdown chart does.
window.rigNavigateGuest = (to) => {
document
.querySelector("#mount iframe")
.contentWindow.postMessage({ __rigCommand: "navigate", to }, SUPERSET_ORIGIN);
log("host", "asked the dashboard to navigate to", to);
};
refreshStats();
</script>
</body>
</html>

View File

@@ -0,0 +1,199 @@
/*
* 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.
*/
// Two origins, because that is what the SDK actually deals with: the host app
// on one, the Superset instance on the other. No dependencies.
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
const sdkRoot = join(here, "..");
export const HOST_PORT = Number(process.env.RIG_HOST_PORT || 8100);
export const SUPERSET_PORT = Number(process.env.RIG_SUPERSET_PORT || 8200);
const hostOrigin = `http://localhost:${HOST_PORT}`;
const supersetOrigin = `http://localhost:${SUPERSET_PORT}`;
/** Every guest token this rig has minted, so a test can count them. */
const minted = [];
function base64url(value) {
return Buffer.from(value).toString("base64url");
}
/** A JWT the SDK can actually decode, since it reads `exp` off the token. */
function mintGuestToken(ttlSeconds) {
const claims = {
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
user: { username: "rig" },
resources: [{ type: "dashboard", id: "rig-dashboard" }],
serial: minted.length + 1,
};
const token = `${base64url('{"alg":"HS256","typ":"JWT"}')}.${base64url(
JSON.stringify(claims),
)}.rig-signature`;
minted.push({ at: Date.now(), ttlSeconds, serial: claims.serial });
return token;
}
function send(res, status, body, type = "text/html; charset=utf-8") {
res.writeHead(status, {
"content-type": type,
"cache-control": "no-store",
// The host page needs to call the token endpoint from its own origin only,
// but the Superset origin loads nothing from here, so this stays simple.
"access-control-allow-origin": "*",
});
res.end(body);
}
async function sendFile(res, path, type) {
try {
send(res, 200, await readFile(path), type);
} catch (err) {
send(res, 404, `not found: ${path}\n${err.message}`, "text/plain");
}
}
// ---------------------------------------------------------------- host app
const hostServer = createServer(async (req, res) => {
const url = new URL(req.url, hostOrigin);
if (url.pathname === "/" || url.pathname === "/index.html") {
const html = await readFile(join(here, "host.html"), "utf8");
return send(
res,
200,
html.replace("__SUPERSET_ORIGIN__", supersetOrigin),
);
}
// The SDK exactly as a host app consumes it: the built UMD bundle.
if (url.pathname === "/sdk.js") {
return sendFile(res, join(sdkRoot, "bundle", "index.js"), "text/javascript");
}
if (url.pathname === "/sdk.js.map") {
return sendFile(
res,
join(sdkRoot, "bundle", "index.js.map"),
"application/json",
);
}
// The host app's own guest-token endpoint. Counting the calls to it is how
// the rig proves the SDK does not mint a token per stray navigation.
if (url.pathname === "/guest-token") {
const ttl = Number(url.searchParams.get("ttl") || 300);
return send(
res,
200,
JSON.stringify({ token: mintGuestToken(ttl) }),
"application/json",
);
}
if (url.pathname === "/stats") {
return send(
res,
200,
JSON.stringify({ tokensMinted: minted.length, minted }),
"application/json",
);
}
if (url.pathname === "/reset") {
minted.length = 0;
return send(res, 200, JSON.stringify({ ok: true }), "application/json");
}
return send(res, 404, "not found", "text/plain");
});
// ------------------------------------------------------------ fake superset
const supersetServer = createServer(async (req, res) => {
const url = new URL(req.url, supersetOrigin);
// What the SDK points the iframe at: /embedded/<uuid>
if (url.pathname.startsWith("/embedded/")) {
return sendFile(res, join(here, "embedded.html"), "text/html; charset=utf-8");
}
// A Superset page that is not the embedded one — what a link in a Markdown
// chart can reach. It listens for nothing.
if (url.pathname === "/plain") {
return send(
res,
200,
`<!doctype html><meta charset="utf-8"><title>a page that is not embedded</title>
<body style="font:14px system-ui;padding:24px">
<h1>Not the embedded page</h1>
<p>No switchboard here. Nothing will answer the SDK's handshake.</p>
<p><a id="back" href="/embedded/rig-dashboard?page=back">back to the dashboard</a></p>`,
);
}
if (url.pathname === "/vendor/switchboard.js") {
return sendFile(
res,
join(sdkRoot, "node_modules/@superset-ui/switchboard/esm/switchboard.js"),
"text/javascript",
);
}
return send(res, 404, "not found", "text/plain");
});
// Rejects, rather than hanging, when either port cannot be bound (typically a
// previous run still holding it), after closing whichever server did come up.
export async function start() {
const servers = [
[hostServer, HOST_PORT],
[supersetServer, SUPERSET_PORT],
];
const stop = () => {
for (const [server] of servers) if (server.listening) server.close();
};
const outcomes = await Promise.allSettled(
servers.map(
([server, port]) =>
new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, () => {
server.off("error", reject);
resolve();
});
}),
),
);
const failure = outcomes.find((o) => o.status === "rejected");
if (failure) {
stop();
throw new Error(
`the rig could not start: ${failure.reason.message} ` +
"(set RIG_HOST_PORT / RIG_SUPERSET_PORT to use other ports)",
{ cause: failure.reason },
);
}
return { hostOrigin, supersetOrigin, stop };
}
// `node server.mjs` runs the rig for a human; the driver imports `start`.
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const { hostOrigin: h } = await start();
console.log(`test rig up:
host app ${h}
fake superset ${supersetOrigin}
open the host app and use the buttons. Ctrl-C to stop.`);
}

View File

@@ -1,6 +1,4 @@
coverage/*
cypress/screenshots
cypress/videos
playwright/.auth
playwright-report/
test-results/

View File

@@ -90,7 +90,7 @@ export default {
'@babel/plugin-transform-export-namespace-from',
],
},
// build instrumented code for testing code coverage with Cypress
// build instrumented code for testing code coverage with E2E tests
instrumented: {
plugins: [
[

View File

@@ -1,4 +0,0 @@
screenshots
.nyc_output
coverage
coverage.json

View File

@@ -1,84 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from 'cypress';
const { verifyDownloadTasks } = require('cy-verify-downloads');
export default defineConfig({
chromeWebSecurity: false,
defaultCommandTimeout: 8000,
numTestsKeptInMemory: 3,
// Disabled after realizing this MESSES UP rison encoding in intricate ways
experimentalFetchPolyfill: false,
experimentalMemoryManagement: true,
requestTimeout: 10000,
video: false,
viewportWidth: 1280,
viewportHeight: 1024,
projectId: 'ud5x2f',
retries: {
runMode: 2,
openMode: 0,
},
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
// ECONNRESET on Chrome/Chromium 117.0.5851.0 when using Cypress <12.15.0
// Check https://github.com/cypress-io/cypress/issues/27804 for context
// TODO: This workaround should be removed when upgrading Cypress
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
// eslint-disable-next-line no-param-reassign
launchOptions.args = launchOptions.args.map(arg => {
if (arg === '--headless') {
return '--headless=new';
}
return arg;
});
launchOptions.args.push(
'--disable-dev-shm-usage',
'--disable-gpu',
'--no-sandbox',
'--disable-software-rasterizer',
'--memory-pressure-off',
'--js-flags=--max-old-space-size=4096',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
);
}
return launchOptions;
});
// eslint-disable-next-line global-require
require('@cypress/code-coverage/task')(on, config);
on('task', verifyDownloadTasks);
// eslint-disable-next-line global-require,import/extensions
return config;
},
baseUrl: 'http://localhost:8088',
excludeSpecPattern: ['**/_skip.*'],
experimentalRunAllSpecs: true,
specPattern: ['cypress/e2e/**/*.{js,jsx,ts,tsx}'],
},
});

View File

@@ -1,38 +0,0 @@
[
{
"slice_name": "1 - Sample chart",
"description": "chart description",
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
"datasource_type": "table",
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
},
{
"slice_name": "2 - Sample chart",
"description": "chart description",
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
"datasource_type": "table",
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
},
{
"slice_name": "3 - Sample chart",
"description": "chart description",
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
"datasource_type": "table",
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
},
{
"slice_name": "4 - Sample chart",
"description": "chart description",
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
"datasource_type": "table",
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
}
]

View File

@@ -1,46 +0,0 @@
[
{
"dashboard_title": "1 - Sample dashboard",
"slug": "1-sample-dashboard"
},
{
"dashboard_title": "2 - Sample dashboard",
"slug": "2-sample-dashboard"
},
{
"dashboard_title": "3 - Sample dashboard",
"slug": "3-sample-dashboard"
},
{
"dashboard_title": "4 - Sample dashboard",
"slug": "4-sample-dashboard"
},
{
"dashboard_title": "5 - Sample dashboard",
"slug": "5-sample-dashboard"
},
{
"dashboard_title": "6 - Sample dashboard",
"slug": "6-sample-dashboard"
},
{
"dashboard_title": "7 - Sample dashboard",
"slug": "7-sample-dashboard"
},
{
"dashboard_title": "8 - Sample dashboard",
"slug": "8-sample-dashboard"
},
{
"dashboard_title": "9 - Sample dashboard",
"slug": "9-sample-dashboard"
},
{
"dashboard_title": "10 - Sample dashboard",
"slug": "10-sample-dashboard"
},
{
"dashboard_title": "11 - Sample dashboard",
"slug": "11-sample-dashboard"
}
]

View File

@@ -1,455 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import '@cypress/code-coverage/support';
import { expect } from 'chai';
import rison from 'rison';
/* eslint-disable @typescript-eslint/no-explicit-any */
require('cy-verify-downloads').addCustomCommand();
// fail on console error, allow config to override individual tests
// these exceptions are a little pile of tech debt
//
// DISABLING FOR NOW
/*
const { getConfig, setConfig } = failOnConsoleError({
consoleMessages: [
/\[webpack-dev-server\]/,
'The pseudo class ":first-child" is potentially unsafe when doing server-side rendering. Try changing it to ":first-of-type".',
'The pseudo class ":nth-child" is potentially unsafe when doing server-side rendering. Try changing it to ":nth-of-type".',
'Error: Unknown Error',
/Unable to infer path to ace from script src/,
],
includeConsoleTypes: ['error'],
});
*/
// Set individual tests to allow certain console errors to NOT fail, e.g
// cy.allowConsoleErrors(['foo', /^some bar-regex.*/]);
// This will be reset between tests.
Cypress.Commands.addAll({
getConsoleMessages: () => cy.wrap(getConfig()?.consoleMessages),
allowConsoleErrors: (consoleMessages: (string | RegExp)[]) =>
setConfig({ ...getConfig(), consoleMessages }),
});
const BASE_EXPLORE_URL = '/explore/?form_data=';
let DASHBOARD_FIXTURES: Record<string, any>[] = [];
let CHART_FIXTURES: Record<string, any>[] = [];
Cypress.Commands.add('loadChartFixtures', () =>
cy.fixture('charts.json').then(charts => {
CHART_FIXTURES = charts;
}),
);
Cypress.Commands.add('loadDashboardFixtures', () =>
cy.fixture('dashboards.json').then(dashboards => {
DASHBOARD_FIXTURES = dashboards;
}),
);
const PATHS_TO_SKIP_LOGIN = ['login', 'register'];
const skipLogin = () => {
for (const path of PATHS_TO_SKIP_LOGIN) {
if (Cypress.currentTest.title.toLowerCase().includes(path)) {
return true;
}
}
return false;
};
before(() => {
if (skipLogin()) {
return;
}
cy.login();
Cypress.Cookies.defaults({ preserve: 'session' });
cy.loadChartFixtures();
cy.loadDashboardFixtures();
});
beforeEach(() => {
if (skipLogin()) {
return;
}
cy.cleanDashboards();
cy.cleanCharts();
});
Cypress.Commands.add('cleanDashboards', () => {
cy.getDashboards().then((sampleDashboards?: Record<string, any>[]) => {
const deletableDashboards = [];
for (let i = 0; i < DASHBOARD_FIXTURES.length; i += 1) {
const fixture = DASHBOARD_FIXTURES[i];
const isInDb = sampleDashboards?.find(
d => d.dashboard_title === fixture.dashboard_title,
);
if (isInDb) {
deletableDashboards.push(isInDb.id);
}
}
if (deletableDashboards.length) {
cy.request({
failOnStatusCode: false,
method: 'DELETE',
url: `api/v1/dashboard/?q=!(${deletableDashboards.join(',')})`,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
}).then(resp => resp);
}
});
});
Cypress.Commands.add('cleanCharts', () => {
cy.getCharts().then((sampleCharts?: Record<string, any>[]) => {
const deletableCharts = [];
for (let i = 0; i < CHART_FIXTURES.length; i += 1) {
const fixture = CHART_FIXTURES[i];
const isInDb = sampleCharts?.find(
c => c.slice_name === fixture.slice_name,
);
if (isInDb) {
deletableCharts.push(isInDb.id);
}
}
if (deletableCharts.length) {
cy.request({
failOnStatusCode: false,
method: 'DELETE',
url: `api/v1/chart/?q=!(${deletableCharts.join(',')})`,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
}).then(resp => resp);
}
});
});
Cypress.Commands.add('getBySel', (selector, ...args) =>
cy.get(`[data-test=${selector}]`, ...args),
);
Cypress.Commands.add('getBySelLike', (selector, ...args) =>
cy.get(`[data-test*=${selector}]`, ...args),
);
/* eslint-disable consistent-return */
Cypress.on('uncaught:exception', err => {
// ignore ResizeObserver client errors, as they are unrelated to operation
// and causing flaky test failures in CI
if (err.message && /ResizeObserver loop limit exceeded/.test(err.message)) {
// returning false here prevents Cypress from failing the test
return false;
}
return false; // TODO:@geido remove
});
/* eslint-enable consistent-return */
Cypress.Commands.add('login', () => {
cy.request({
method: 'POST',
url: '/login/',
body: { username: 'admin', password: 'general' },
}).then(response => {
if (response.status === 302) {
// If there's a redirect, follow it manually
const redirectUrl = response.headers.location;
cy.request({
method: 'GET',
url: redirectUrl,
}).then(finalResponse => {
expect(finalResponse.status).to.eq(200);
});
} else {
expect(response.status).to.eq(200);
}
});
});
Cypress.Commands.add('visitChartByName', name => {
const query = rison.encode({
columns: ['id'],
filters: [{ col: 'slice_name', opr: 'eq', value: name }],
});
cy.request(`/api/v1/chart?q=${query}`).then(response => {
cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${response.body.result[0].id}}`);
});
});
Cypress.Commands.add('visitChartById', chartId =>
cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`),
);
Cypress.Commands.add(
'visitChartByParams',
(formData: {
datasource?: string;
datasource_id?: number;
datasource_type?: string;
[key: string]: unknown;
}) => {
let datasource_id;
let datasource_type;
if (formData.datasource_id && formData.datasource_type) {
({ datasource_id, datasource_type } = formData);
} else {
[datasource_id, datasource_type] = formData.datasource?.split('__') || [];
}
const accessToken = window.localStorage.getItem('access_token');
cy.request({
method: 'POST',
url: 'api/v1/explore/form_data',
body: {
datasource_id,
datasource_type,
form_data: JSON.stringify(formData),
},
headers: {
...(accessToken && {
Cookie: `csrf_access_token=${accessToken}`,
'X-CSRFToken': accessToken,
}),
'Content-Type': 'application/json',
Referer: `${Cypress.config().baseUrl}/`,
},
}).then(response => {
const formDataKey = response.body.key;
const url = `/explore/?form_data_key=${formDataKey}`;
cy.visit(url);
});
},
);
Cypress.Commands.add('verifySliceContainer', chartSelector => {
// After a wait response check for valid slice container
cy.get('.slice_container')
.should('be.visible')
.within(() => {
if (chartSelector) {
cy.get(chartSelector)
.should('be.visible')
.then(chart => {
expect(chart[0].clientWidth).greaterThan(0);
expect(chart[0].clientHeight).greaterThan(0);
});
}
});
return cy;
});
Cypress.Commands.add(
'verifySliceSuccess',
({
waitAlias,
querySubstring,
chartSelector,
}: {
waitAlias: string;
chartSelector: JQuery.Selector;
querySubstring?: string | RegExp;
}) => {
cy.wait(waitAlias).then(({ response }) => {
cy.verifySliceContainer(chartSelector);
const responseBody = response?.body;
if (querySubstring) {
const query: string =
responseBody.query || responseBody.result[0].query || '';
if (querySubstring instanceof RegExp) {
expect(query).to.match(querySubstring);
} else {
expect(query).to.contain(querySubstring);
}
}
});
return cy;
},
);
Cypress.Commands.add('createSampleDashboards', (indexes?: number[]) =>
cy.cleanDashboards().then(() => {
for (let i = 0; i < DASHBOARD_FIXTURES.length; i += 1) {
if (indexes?.includes(i) || !indexes) {
cy.request({
method: 'POST',
url: `/api/v1/dashboard/`,
body: DASHBOARD_FIXTURES[i],
failOnStatusCode: false,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
});
}
}
}),
);
Cypress.Commands.add('createSampleCharts', (indexes?: number[]) =>
cy.cleanCharts().then(() => {
for (let i = 0; i < CHART_FIXTURES.length; i += 1) {
if (indexes?.includes(i) || !indexes) {
cy.request({
method: 'POST',
url: `/api/v1/chart/`,
body: CHART_FIXTURES[i],
failOnStatusCode: false,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
});
}
}
}),
);
Cypress.Commands.add(
'deleteDashboardByName',
(dashboardName: string, failOnStatusCode = false) =>
cy.getDashboards().then((sampleDashboards?: Record<string, any>[]) => {
const dashboard = sampleDashboards?.find(
d => d.dashboard_title === dashboardName,
);
if (dashboard) {
cy.deleteDashboard(dashboard.id, failOnStatusCode);
}
}),
);
Cypress.Commands.add(
'deleteDashboard',
(id: number, failOnStatusCode = false) =>
cy
.request({
failOnStatusCode,
method: 'DELETE',
url: `api/v1/dashboard/${id}`,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
})
.then(resp => resp),
);
Cypress.Commands.add('getDashboards', () => {
cy.request({
method: 'GET',
url: `api/v1/dashboard/`,
headers: {
'Content-Type': 'application/json',
},
}).then(resp => resp.body.result);
});
Cypress.Commands.add('getDashboard', (dashboardId: string | number) =>
cy
.request({
method: 'GET',
url: `api/v1/dashboard/${dashboardId}`,
headers: {
'Content-Type': 'application/json',
},
})
.then(resp => resp.body.result),
);
Cypress.Commands.add(
'updateDashboard',
(dashboardId: number, body: Record<string, any>) =>
cy
.request({
method: 'PUT',
url: `api/v1/dashboard/${dashboardId}`,
body,
headers: {
'Content-Type': 'application/json',
},
})
.then(resp => resp.body.result),
);
Cypress.Commands.add('deleteChart', (id: number, failOnStatusCode = false) =>
cy
.request({
failOnStatusCode,
method: 'DELETE',
url: `api/v1/chart/${id}`,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
})
.then(resp => resp),
);
Cypress.Commands.add('getCharts', () =>
cy
.request({
method: 'GET',
url: `api/v1/chart/`,
headers: {
'Content-Type': 'application/json',
},
})
.then(resp => resp.body.result),
);
Cypress.Commands.add(
'deleteChartByName',
(sliceName: string, failOnStatusCode = false) =>
cy.getCharts().then((sampleCharts?: Record<string, any>[]) => {
const chart = sampleCharts?.find(c => c.slice_name === sliceName);
if (chart) {
cy.deleteChart(chart.id, failOnStatusCode);
}
}),
);

View File

@@ -1,92 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// eslint-disable-next-line spaced-comment
/// <reference types="cypress" />
type JSONPrimitive = string | number | boolean | null;
type JSONValue = JSONPrimitive | JSONObject | JSONArray;
type JSONObject = { [member: string]: JSONValue };
type JSONArray = JSONValue[];
declare namespace Cypress {
interface Chainable {
/**
* Login test user.
*/
login(): void;
/**
*
* Utils
*/
getBySel(selector: string): cy;
getBySelLike(selector: string): cy;
cleanCharts(): cy;
cleanDashboards(): cy;
loadChartFixtures(): cy;
loadDashboardFixtures(): cy;
allowConsoleErrors(consoleMessages: (string | RegExp)[]): cy;
visitChartByParams(params: string | Record<string, unknown>): cy;
visitChartByName(name: string): cy;
visitChartById(id: number): cy;
/**
* Verify slice container renders.
*/
verifySliceContainer(chartSelector: JQuery.Selector): cy;
/**
* Verify slice successfully loaded.
*/
verifySliceSuccess(options: {
waitAlias: string;
querySubstring?: string | RegExp;
chartSelector?: JQuery.Selector;
}): cy;
/**
* Get
*/
getDashboards(): cy;
getDashboard(dashboardId: string | number): Record<string, any>;
getCharts(): cy;
/**
* Create
*/
createSampleDashboards(indexes?: number[]): void;
createSampleCharts(indexes?: number[]): void;
/**
* Delete
*/
deleteDashboard(id: number, failOnStatusCode: boolean): cy;
deleteDashboardByName(dashboardName: string, failOnStatusCode: boolean): cy;
deleteChartByName(name: string, failOnStatusCode: boolean): cy;
deleteChart(id: number, failOnStatusCode: boolean): cy;
/**
* Update
*/
updateDashboard(dashboardId: number, body: Record<string, any>): cy;
}
}
declare module '@cypress/code-coverage/task';

View File

@@ -1,121 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getChartAlias, Slice } from 'cypress/utils/vizPlugins';
export * from './vizPlugins';
export { default as parsePostForm } from './parsePostForm';
export interface ChartSpec {
name: string;
viz: string;
}
const toSlicelike = ($chart: JQuery<HTMLElement>): Slice => {
const chartId = $chart.attr('data-test-chart-id');
const vizType = $chart.attr('data-test-viz-type');
return {
slice_id: chartId ? parseInt(chartId, 10) : null,
form_data: {
viz_type: vizType || null,
},
};
};
export function getChartGridComponent({ name, viz }: ChartSpec) {
return cy
.get(`[data-test-chart-name="${name}"]`)
.should('have.attr', 'data-test-viz-type', viz);
}
export function getChartAliasBySpec(chart: ChartSpec) {
return getChartGridComponent(chart).then($chart =>
cy.wrap(getChartAlias(toSlicelike($chart))),
);
}
export function getChartAliasesBySpec(charts: readonly ChartSpec[]) {
const aliases: string[] = [];
charts.forEach(chart =>
getChartAliasBySpec(chart).then(alias => {
aliases.push(alias);
}),
);
// Wrapping the aliases is key.
// That way callers can chain off this function
// and actually get the list of aliases.
return cy.wrap(aliases);
}
export function waitForChartLoad(chart: ChartSpec) {
return getChartGridComponent(chart).then(gridComponent => {
const chartId = gridComponent.attr('data-test-chart-id');
// the chart should load in under half a minute
return (
cy
// this id only becomes visible when the chart is loaded
.get(`#chart-id-${chartId}`, {
timeout: 30000,
})
.should('be.visible')
// return the chart grid component
.then(() => gridComponent)
);
});
}
export function resize(selector: string) {
return {
to(cordX: number, cordY: number) {
cy.get(selector).trigger('mousedown', { which: 1, force: true });
cy.get(selector).trigger('mousemove', {
which: 1,
cordX,
cordY,
force: true,
});
cy.get(selector).trigger('mouseup', { which: 1, force: true });
},
};
}
export const setSelectSearchInput = (
$input: any,
value: string,
async = false,
) => {
// Ant Design 5 Select crashes Chromium with type/click events when showSearch is true.
// This copies the value directly to the input element as a workaround.
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value',
)?.set;
nativeInputValueSetter?.call($input[0], value);
// Trigger the input and change events
if (async) {
$input[0].dispatchEvent(new Event('mousedown', { bubbles: true }));
}
$input[0].dispatchEvent(new Event('input', { bubbles: true }));
$input[0].dispatchEvent(new Event('change', { bubbles: true }));
cy.get('.ant-select-item-option-content').should('exist').first().click({
force: true,
});
};

View File

@@ -1,60 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Parse multipart form data sent via POST requests.
*/
export default function parsePostForm(requestBody: ArrayBuffer) {
type ParsedFields = Record<string, string[] | string>;
if (requestBody.constructor.name !== 'ArrayBuffer') {
return requestBody;
}
const lines = new TextDecoder('utf-8').decode(requestBody).split('\n');
const fields: ParsedFields = {};
let key = '';
let value: string[] = [];
function addField(key: string, value: string) {
if (key in fields) {
if (Array.isArray(fields[key])) {
(fields[key] as string[]).push(value);
} else {
fields[key] = [fields[key] as string, value];
}
} else {
fields[key] = value;
}
}
lines.forEach(line => {
const nameMatch = line.match(/Content-Disposition: form-data; name="(.*)"/);
if (nameMatch) {
if (key) {
addField(key, value.join('\n'));
}
key = nameMatch[1];
value = [];
} else if (!/----.*FormBoundary/.test(line)) {
value.push(line);
}
});
if (key && value) {
addField(key, value.join('\n'));
}
return fields;
}

View File

@@ -1,104 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
export type JsonArray = JsonValue[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type JsonObject = { [member: string]: any };
export interface Slice {
slice_id: number;
form_data: {
viz_type: string;
};
}
export interface Dashboard {
slices: Slice[];
}
const V1_PLUGINS = [
'box_plot',
'echarts_timeseries',
'word_cloud',
'pie',
'table',
'big_number',
'big_number_total',
];
export const DASHBOARD_CHART_ALIAS_PREFIX = 'getChartData_';
export function isLegacyChart(vizType: string): boolean {
return !V1_PLUGINS.includes(vizType);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isLegacyResponse(response: any): boolean {
return !response.result;
}
export function getSliceIdFromRequestUrl(url: string) {
const address = new URL(url);
const query = address.searchParams.get('form_data');
return query?.match(/\d+/)?.[0];
}
export function getChartDataRouteForSlice(slice: Slice) {
const vizType = slice.form_data.viz_type;
const isLegacy = isLegacyChart(vizType);
const formData = encodeURIComponent(`{"slice_id":${slice.slice_id}}`);
if (isLegacy) {
return `**/superset/explore_json/?*${formData}*`;
}
return `**/api/v1/chart/data?*${formData}*`;
}
export function getChartAlias(slice: Slice): string {
const alias = `${DASHBOARD_CHART_ALIAS_PREFIX}${slice.slice_id}_${slice.form_data.viz_type}`;
const route = getChartDataRouteForSlice(slice);
cy.intercept('POST', route).as(alias);
return `@${alias}`;
}
export function getChartAliases(slices: Slice[]): string[] {
return Array.from(slices).map(getChartAlias);
}
export function interceptChart({
sliceId,
legacy = false,
method = 'POST',
}: {
sliceId?: number;
legacy?: boolean;
method?: 'POST' | 'GET';
}) {
const urlBase = legacy ? '**/superset/explore_json/' : '**/api/v1/chart/data';
let url;
if (sliceId) {
const encodedFormData = encodeURIComponent(
JSON.stringify({ slice_id: sliceId }),
);
url = `${urlBase}?form_data=${encodedFormData}*`;
} else {
url = `${urlBase}**`;
}
return cy.intercept(method, url);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +0,0 @@
{
"name": "superset-cypress",
"version": "1.0.0",
"description": "run cypress against superset",
"license": "Apache-2.0",
"author": "Apache",
"scripts": {
"cypress": "cypress",
"cypress-debug": "cypress open --config watchForFileChanges=true",
"cypress-run-chrome": "cypress run --browser chrome --headless"
},
"dependencies": {
"@cypress/code-coverage": "^3.10.4",
"@superset-ui/core": "^2.1.0",
"brace": "^0.11.1",
"chai": "^4.5.0",
"cy-verify-downloads": "^0.2.5",
"cypress-fail-on-console-error": "^4.0.3",
"nanoid": "^6.0.0",
"querystringify": "^2.2.0",
"react-dom": "^16.13.0",
"rison": "^0.1.1"
},
"devDependencies": {
"@types/querystringify": "^2.0.0",
"cypress": "^11.2.0",
"eslint-plugin-cypress": "^3.5.0",
"tscw-config": "^1.1.2"
},
"overrides": {
"@babel/core": "^7.29.6",
"@cypress/code-coverage": {
"js-yaml": "^4.3.2"
},
"@cypress/request": "^3.0.0",
"cypress": {
"form-data": "^2.3.4"
},
"d3-interpolate": {
"d3-color": "3.1.0"
},
"minimatch@<10": {
"brace-expansion": "1.1.18"
},
"minimatch@>=10": {
"brace-expansion": ">=5.0.9"
},
"qs": "^6.14.2",
"uuid": "^11.1.1"
},
"nyc": {
"reporter": [
"html",
"json"
]
}
}

View File

@@ -1,16 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"allowSyntheticDefaultImports": true,
"moduleResolution": "Node",
"strict": true,
"target": "es2019",
"lib": ["es2019", "DOM"],
"types": ["cypress", "node"],
"allowJs": true,
"noEmit": true
},
"files": ["cypress/support/index.d.ts"],
"include": ["cypress/**/*.ts", "./cypress.config.ts"],
"exclude": ["node_modules"]
}

View File

@@ -1,46 +0,0 @@
#!/bin/bash
#
# 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.
#
set -e
cd "$(dirname "$0")"
#run all the python steps in a background process
time superset db upgrade
time superset load_test_users
time superset load_examples --load-test-data
time superset init
echo "[completed python build steps]"
PORT='8081'
flask run -p $PORT --with-threads --reload --debugger &
#block on the longer running javascript process
time npm ci
time npm run build-instrumented
echo "[completed js build steps]"
#setup cypress
cd cypress-base
time npm ci
export CYPRESS_BASE_URL="http://localhost:${PORT}"
if [ -n "$1" ]; then
CYPRESS_PATH='cypress/e2e/'${1}'/*'
time npm run cypress-run-chrome -- --spec "$CYPRESS_PATH" --record false --config video=false || true
else
time npm run cypress-run-chrome -- --record false --config video=false || true
fi
kill %1

View File

@@ -120,7 +120,6 @@ export default [
'**/spec/**',
'**/__tests__/**',
'**/__mocks__/**',
'cypress-base/**',
],
rules: {
'theme-colors/no-literal-colors': 'off',

View File

@@ -43,6 +43,12 @@ export default {
// lodash-es is ESM (type: module) which jest.mock cannot intercept; alias to
// the CJS lodash build (identical API) so module mocks work in tests.
'^lodash-es$': '<rootDir>/node_modules/lodash',
// @deck.gl/maplibre@9.4.0 ships ESM-only (no dist/*.cjs, no "require"
// export condition), unlike every other @deck.gl/* package - force
// resolution to its ESM entry point directly; transformIgnorePatterns
// below still needs to let it (and its relative imports) through babel.
'^@deck.gl/maplibre$':
'<rootDir>/node_modules/@deck.gl/maplibre/dist/index.js',
'^src/(.*)$': '<rootDir>/src/$1',
'^spec/(.*)$': '<rootDir>/spec/$1',
// mapping plugins of superset-ui to source code
@@ -98,7 +104,11 @@ export default {
// gettext-parser is ESM-only too, and scripts/po2json.js (which
// spec/scripts/po2json.test.ts exercises directly against the real
// package, not a mock, to catch a parsing regression) imports it.
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format|selection)|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/*.|@x0k/.*|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|react-markdown|vfile|web-namespaces|html-void-elements|html-url-attributes|estree-util-is-identifier-name|trim-lines|is-plain-obj|trough|unist-.*|hast-.*|hastscript|refractor|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|character-reference-invalid|is-alphanumerical|is-alphabetical|is-decimal|is-hexadecimal|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|@x0k/json-schema-merge|content-disposition|gettext-parser)',
//
// @deck.gl/maplibre@9.4.0 ships ESM-only (no dist/*.cjs), unlike every
// other @deck.gl/* package - allowed through so babel-jest can convert
// its import/export syntax (see the moduleNameMapper entry above).
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format|selection)|internmap|@mapbox/tiny-sdf|@deck.gl/maplibre|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/*.|@x0k/.*|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|react-markdown|vfile|web-namespaces|html-void-elements|html-url-attributes|estree-util-is-identifier-name|trim-lines|is-plain-obj|trough|unist-.*|hast-.*|hastscript|refractor|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|character-reference-invalid|is-alphanumerical|is-alphabetical|is-decimal|is-hexadecimal|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|@x0k/json-schema-merge|content-disposition|gettext-parser)',
],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',

View File

@@ -314,7 +314,6 @@
],
"ignorePatterns": [
"packages/generator-superset/**/*",
"cypress-base/**",
"**/node_modules/**",
"build/**",
"**/dist/**",

File diff suppressed because it is too large Load Diff

View File

@@ -38,7 +38,7 @@
],
"type": "module",
"scripts": {
"_format": "oxfmt './{src,spec,cypress-base,plugins,packages,.storybook}/**/*{.js,.jsx,.ts,.tsx,.css,.scss,.sass,.json}'",
"_format": "oxfmt './{src,spec,plugins,packages,.storybook}/**/*{.js,.jsx,.ts,.tsx,.css,.scss,.sass,.json}'",
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --color --mode production",
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --color",
"build-instrumented": "cross-env NODE_ENV=production BABEL_ENV=instrumented webpack --mode=production --color",
@@ -93,13 +93,13 @@
"dependencies": {
"@apache-superset/core": "file:packages/superset-core",
"@braintree/sanitize-url": "^7.1.2",
"@deck.gl/aggregation-layers": "~9.2.5",
"@deck.gl/core": "~9.2.5",
"@deck.gl/extensions": "~9.2.5",
"@deck.gl/geo-layers": "~9.2.5",
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mesh-layers": "~9.2.5",
"@deck.gl/react": "~9.2.5",
"@deck.gl/aggregation-layers": "~9.4.0",
"@deck.gl/core": "~9.4.0",
"@deck.gl/extensions": "~9.4.0",
"@deck.gl/geo-layers": "~9.4.0",
"@deck.gl/layers": "~9.4.0",
"@deck.gl/mesh-layers": "~9.4.0",
"@deck.gl/react": "~9.4.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
@@ -110,21 +110,21 @@
"@fontsource/fira-code": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/inter": "^5.3.0",
"@googleapis/sheets": "^18.0.0",
"@googleapis/sheets": "^18.0.1",
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
"@jsonforms/core": "^3.7.0",
"@jsonforms/react": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.8.0",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
"@luma.gl/engine": "~9.2.5",
"@luma.gl/gltf": "~9.2.5",
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@luma.gl/constants": "~9.4.0",
"@luma.gl/core": "~9.4.0",
"@luma.gl/engine": "~9.4.0",
"@luma.gl/gltf": "~9.4.0",
"@luma.gl/shadertools": "~9.4.0",
"@luma.gl/webgl": "~9.4.0",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^6.10.1",
"@rjsf/core": "^6.11.0",
"@rjsf/utils": "^6.6.2",
"@rjsf/validator-ajv8": "^6.10.1",
"@rjsf/validator-ajv8": "^6.11.0",
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",
@@ -163,7 +163,7 @@
"content-disposition": "^3.0.0",
"d3-scale": "^4.0.2",
"dayjs": "^1.11.23",
"dom-to-image-more": "^3.10.2",
"dom-to-image-more": "^3.11.0",
"dom-to-pdf": "^0.3.2",
"echarts": "^6.1.0",
"fuse.js": "^7.5.0",
@@ -174,6 +174,7 @@
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^11.1.0",
"html2canvas": "^1.4.1",
"immer": "^11.1.18",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -260,9 +261,9 @@
"@superset-ui/eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"@superset-ui/eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.16.2",
"@swc/plugin-emotion": "^16.0.0",
"@swc/plugin-transform-imports": "^14.0.0",
"@swc/core": "^1.16.12",
"@swc/plugin-emotion": "^17.0.0",
"@swc/plugin-transform-imports": "^15.0.0",
"@testing-library/dom": "^10.4.2",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
@@ -275,7 +276,7 @@
"@types/json-bigint": "^1.0.4",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
"@types/node": "^26.6.2",
"@types/node": "^26.6.3",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react-redux": "^7.1.10",
@@ -285,13 +286,13 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.70.1",
"@typescript-eslint/eslint-plugin": "^8.71.0",
"@typescript-eslint/parser": "^8.63.0",
"babel-jest": "^30.5.2",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"baseline-browser-mapping": "^2.11.25",
"baseline-browser-mapping": "^2.11.26",
"cheerio": "1.2.0",
"concurrently": "^10.0.5",
"copy-webpack-plugin": "^14.0.0",
@@ -323,11 +324,11 @@
"lerna": "^10.0.1",
"lightningcss": "^1.33.0",
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.11.0",
"minimizer-webpack-plugin": "^5.12.0",
"open-cli": "^9.0.0",
"oxfmt": "^0.70.0",
"oxlint": "^1.85.0",
"postcss-styled-syntax": "^0.7.2",
"oxlint": "^1.86.0",
"postcss-styled-syntax": "^0.7.3",
"process": "^0.11.10",
"react-refresh": "^0.19.0",
"react-resizable": "^4.0.2",
@@ -343,13 +344,13 @@
"tsx": "^4.23.15",
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"wait-on": "^9.1.0",
"wait-on": "^9.4.0",
"webpack": "^5.111.1",
"webpack-bundle-analyzer": "^5.4.0",
"webpack-cli": "^7.2.3",
"webpack-dev-server": "^6.0.0",
"webpack-manifest-plugin": "^6.0.1",
"webpack-sources": "^3.5.1",
"webpack-sources": "^3.6.0",
"webpack-visualizer-plugin2": "^2.0.0"
},
"peerDependencies": {
@@ -363,14 +364,14 @@
"@babel/eslint-parser": {
"eslint": "$eslint"
},
"@deck.gl/aggregation-layers": "~9.2.5",
"@deck.gl/core": "~9.2.5",
"@deck.gl/extensions": "~9.2.5",
"@deck.gl/geo-layers": "~9.2.5",
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mesh-layers": "~9.2.5",
"@deck.gl/react": "~9.2.5",
"@deck.gl/widgets": "~9.2.5",
"@deck.gl/aggregation-layers": "~9.4.0",
"@deck.gl/core": "~9.4.0",
"@deck.gl/extensions": "~9.4.0",
"@deck.gl/geo-layers": "~9.4.0",
"@deck.gl/layers": "~9.4.0",
"@deck.gl/mesh-layers": "~9.4.0",
"@deck.gl/react": "~9.4.0",
"@deck.gl/widgets": "~9.4.0",
"@great-expectations/jsonforms-antd-renderers": {
"antd": "$antd"
},
@@ -382,17 +383,17 @@
"@loaders.gl/compression": {
"fflate": "^0.7.5"
},
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
"@luma.gl/engine": "~9.2.5",
"@luma.gl/gltf": "~9.2.5",
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@luma.gl/constants": "~9.4.0",
"@luma.gl/core": "~9.4.0",
"@luma.gl/engine": "~9.4.0",
"@luma.gl/gltf": "~9.4.0",
"@luma.gl/shadertools": "~9.4.0",
"@luma.gl/webgl": "~9.4.0",
"core-js": "^3.38.1",
"cosmiconfig": {
"js-yaml": "^4.3.2"
},
"dompurify": "^3.4.13",
"dompurify": "^3.4.16",
"esbuild": "^0.28.1",
"eslint-plugin-import": {
"eslint": "$eslint"

View File

@@ -31,7 +31,7 @@
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
},
"dependencies": {
"chalk": "^6.0.0",
"chalk": "^6.0.1",
"lodash-es": "^4.18.1",
"yeoman-generator": "^9.2.0",
"yosay": "^3.0.0"

View File

@@ -45,6 +45,13 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
['datasource'],
['viz_type'],
[
{
name: 'semantic_selection_version',
config: {
type: 'HiddenControl',
hidden: true,
},
},
{
name: 'slice_id',
config: {

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